Refactor Setup Wizard & Profile Modal Configuration Panes (#1127)

* incorporating configuration panes from #1078

* standardizing player hero subcomponents & removing `owner` var from `useProfileField` 🚪

* switching box type, ambiguating overly-specific names, & massaging heights 📱

* reordering profile details sections, updating deployment environment, & conditionally wrapping the hero elements 🎬

* fixing render of color disposition selector 🕍

* self code review changes: removed some `;`s 🎋

* getting yarn typecheck && yarn lint to pass post rebase 🏇🏾

* handling "missing <div> in <button>" error with mounted check & setting HTTP return codes for OpenSea API endpoint 🕺

* `ProfileWizard` extends `Wizard`, roles display better on mobile, & pronouns use `ProfileWizardPane` 🍊

* properly encapsulating ETH address regex 🚲

* adding some tasks 🏥

* fixed skills layou

* handling default values? 

* corrections for revision comments from @dan13ram (UI bugs to follow) 🌋

* cleaning up memberships & explorer type 🧫

* refactoring player roles configuration and display to use `WizardPane` 🔮

* bunches of mobile fixes & repairing the display of deserialized skills 📟

* removing redirect in static props & formatting memberships display for responsiveness 🪆

* improving comprehensability of `/me` & more mobile tweaking 🍦

* various spacing fixes & a “try again” button for connecting 🫕

* maybe fixed issue with saving images for fields with default values 🇩🇿

* switched roles selection to a bounded `SimpleGrid` to fix sizing weirdness 🧰

* fix: removed verify on brightId button

* fix: clean up username page

* formatting & fixing skills issues 🥩

* reformatting NFT display 🚂

* adding `/join` 🚉

* style: peth's suggestions

* added mainnet required message

* style: slight tweak of megamenu item positions

* chore(release): 0.2.0

Co-authored-by: tenfinney <scott@1box.onmicrosoft.com>
Co-authored-by: dan13ram <dan13ram@gmail.com>
Co-authored-by: vidvidvid <weetopol@gmail.com>
This commit is contained in:
δυς
2022-02-28 14:06:16 -05:00
committed by Scott Stevenson
parent 93395d17ba
commit 4a4538c347
101 changed files with 3392 additions and 3457 deletions

15
.prettierrc.js Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
printWidth: 80,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'always',
// semi: false,
overrides: [
{
files: '*.yaml',
options: {
singleQuote: false,
},
},
],
};

View File

@@ -1,14 +0,0 @@
{
"printWidth": 80,
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "always",
"overrides": [
{
"files": "*.yaml",
"options": {
"singleQuote": false
}
}
]
}

View File

@@ -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 <LoadingState />, 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

View File

@@ -141,9 +141,7 @@ type DiscordGuildAuthResponse {
}
type CacheProcessOutput {
success : Boolean!
queued : Boolean!
error : String
updateIDXProfile : uuid
}
type ExpiredPlayerProfiles {

View File

@@ -36,7 +36,6 @@ actions:
definition:
kind: asynchronous
handler: '{{ACTION_BASE_ENDPOINT}}/idxCache/updateSingle'
forward_client_headers: true
permissions:
- role: player
- role: public

View File

@@ -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
;

View File

@@ -0,0 +1,6 @@
INSERT INTO profile (player_id)
SELECT id FROM player
WHERE id NOT IN (
SELECT player_id FROM profile
)
;

View File

@@ -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",

View File

@@ -80,8 +80,6 @@ export default async (playerId: string): Promise<UpdateIdxProfileResponse> => {
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);
}

View File

@@ -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;
});

View File

@@ -35,7 +35,7 @@ export const typeDefs = gql`
chain: String!
title: String
version: String
avatarUrl: String
avatarURL: String
}
type Member {

View File

@@ -16,8 +16,7 @@ export const MetaButton: React.FC<
fontSize="sm"
bg="purple.400"
color="white"
{...{ ref }}
{...props}
{...{ ref, ...props }}
>
{children}
</Button>

View File

@@ -1,14 +1,14 @@
import { Tag, TagProps } from '@chakra-ui/react';
import React from 'react';
export const MetaTag: React.FC<TagProps> = React.forwardRef<HTMLSpanElement>(
export const MetaTag = React.forwardRef<HTMLSpanElement, TagProps>(
({ children, ...props }, ref) => (
<Tag
fontSize="sm"
fontWeight="bold"
backgroundColor="purpleTag"
color="white"
ref={ref}
{...{ ref }}
{...props}
>
{children}

View File

@@ -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<string, unknown> {
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<Array<string>> = undefined,
) => (tz: TimeZoneType): boolean => {
if (!cityZones) {
// eslint-disable-next-line no-param-reassign

View File

@@ -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<string>;
type StatusedSubmitProps = {
label?: Maybe<string | ReactElement>;
status?: Maybe<string | ReactElement>;
}) => (
};
export const StatusedSubmitButton: React.FC<
StatusedSubmitProps & ButtonProps
> = ({ label = 'Submit', status = null, ...props }) => (
<MetaButton
type="submit"
border="2px solid transparent"
transition="0.25s"
_hover={{ filter: 'hue-rotate(-10deg)', border: '2px solid green' }}
_focus={{ filter: 'brightness(1.75)' }}
_hover={{ filter: 'hue-rotate(-90deg)', border: '2px solid green' }}
disabled={!!status}
mt={10}
{...props}
>
{status == null ? (

View File

@@ -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 }) => (
<Text
as="span"
p={1.5}
fontSize="xs"
color="cyanText"
cursor="pointer"
borderRadius="md"
border="2px solid transparent"
_hover={{
color: 'purple.800',
bg: '#FFFFFF66',
borderColor: 'purple.600',
}}
mr={[2, 0]}
{...{ onClick }}
>
View All{size != null ? ` (${size})` : null}
</Text>
);
export default ViewAllButton;

View File

@@ -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<Props & IconProps> = ({ chain, ...props }) => {
if (chain?.toLowerCase().includes('xdai')) return <XDaiIcon {...props} />;
if (chain?.toLowerCase().includes('polygon'))
return <PolygonIcon {...props} />;
return <EthereumIcon {...props} />;
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 (
<Tooltip label={`on the ${info.name} network`} hasArrow>
<info.Icon {...props} />
</Tooltip>
);
};

View File

@@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="293px" viewBox="0 0 256 293" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M226.011429,0 L29.9885714,0 C13.4582857,0 0,13.4582857 0,30.1348571 L0,227.913143 C0,244.589714 13.4582857,258.048 29.9885714,258.048 L195.876571,258.048 L188.123429,230.985143 L206.848,248.393143 L224.548571,264.777143 L256,292.571429 L256,30.1348571 C256,13.4582857 242.541714,0 226.011429,0 Z M169.545143,191.049143 C169.545143,191.049143 164.278857,184.758857 159.890286,179.2 C179.053714,173.787429 186.368,161.792 186.368,161.792 C180.370286,165.741714 174.665143,168.521143 169.545143,170.422857 C162.230857,173.494857 155.209143,175.542857 148.333714,176.713143 C134.290286,179.346286 121.417143,178.614857 110.445714,176.566857 C102.107429,174.957714 94.9394286,172.617143 88.9417143,170.276571 C85.5771429,168.96 81.92,167.350857 78.2628571,165.302857 C77.824,165.010286 77.3851429,164.864 76.9462857,164.571429 C76.6537143,164.425143 76.5074286,164.278857 76.3611429,164.132571 C73.728,162.669714 72.2651429,161.645714 72.2651429,161.645714 C72.2651429,161.645714 79.2868571,173.348571 97.8651429,178.907429 C93.4765714,184.466286 88.064,191.049143 88.064,191.049143 C55.7348571,190.025143 43.4468571,168.813714 43.4468571,168.813714 C43.4468571,121.709714 64.512,83.5291429 64.512,83.5291429 C85.5771429,67.7302857 105.618286,68.1691429 105.618286,68.1691429 L107.081143,69.9245714 C80.7497143,77.5314286 68.608,89.088 68.608,89.088 C68.608,89.088 71.8262857,87.3325714 77.2388571,84.8457143 C92.8914286,77.9702857 105.325714,76.0685714 110.445714,75.6297143 C111.323429,75.4834286 112.054857,75.3371429 112.932571,75.3371429 C121.856,74.1668571 131.949714,73.8742857 142.482286,75.0445714 C156.379429,76.6537143 171.300571,80.7497143 186.514286,89.088 C186.514286,89.088 174.957714,78.1165714 150.089143,70.5097143 L152.137143,68.1691429 C152.137143,68.1691429 172.178286,67.7302857 193.243429,83.5291429 C193.243429,83.5291429 214.308571,121.709714 214.308571,168.813714 C214.308571,168.813714 201.874286,190.025143 169.545143,191.049143 Z M101.522286,122.733714 C93.184,122.733714 86.6011429,130.048 86.6011429,138.971429 C86.6011429,147.894857 93.3302857,155.209143 101.522286,155.209143 C109.860571,155.209143 116.443429,147.894857 116.443429,138.971429 C116.589714,130.048 109.860571,122.733714 101.522286,122.733714 M154.916571,122.733714 C146.578286,122.733714 139.995429,130.048 139.995429,138.971429 C139.995429,147.894857 146.724571,155.209143 154.916571,155.209143 C163.254857,155.209143 169.837714,147.894857 169.837714,138.971429 C169.837714,130.048 163.254857,122.733714 154.916571,122.733714" fill="#7289DA"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -5,7 +5,7 @@ export const PageContainer: React.FC<FlexProps> = ({ children, ...props }) => (
<Flex
w="100%"
h="100%"
p={{ base: 8, lg: 12 }}
p={{ base: 3, sm: 8, lg: 12 }}
direction="column"
align="center"
pos="relative"
@@ -16,14 +16,7 @@ export const PageContainer: React.FC<FlexProps> = ({ children, ...props }) => (
);
export const FlexContainer: React.FC<StackProps> = ({ children, ...props }) => (
<Stack
w="100%"
align="center"
justify="center"
direction="column"
spacing={8}
{...props}
>
<Stack w="full" align="center" justify="center" spacing={[6, 8]} {...props}>
{children}
</Stack>
);

View File

@@ -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>;
player?: Maybe<Player>;
onClose: () => void;
};
@@ -80,25 +81,43 @@ const Label: React.FC<FormLabelProps> = React.forwardRef(
},
);
const Input: React.FC<InputProps> = React.forwardRef(
({ children, ...props }, reference) => {
const Input = React.forwardRef<typeof ChakraInput, InputProps>(
({ children, ...props }, fwdRef) => {
const [width, setWidth] = useState('9em');
const ref = reference as RefObject<HTMLInputElement>;
const textRef = useRef<HTMLInputElement>(null);
const ref = fwdRef as RefObject<HTMLInputElement>;
const textRef = useRef<HTMLParagraphElement>(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<HTMLInputElement>) => {
if (isText) {
const {
currentTarget: { value },
} = event;
calcWidth(value);
}
};
return (
<Box>
<Text position="absolute" whiteSpace="pre" ref={textRef}></Text>
<Text
position="absolute"
visibility="hidden"
whiteSpace="pre"
ref={textRef}
></Text>
<ChakraInput
color="white"
bg="dark"
@@ -111,20 +130,10 @@ const Input: React.FC<InputProps> = 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}
</ChakraInput>
@@ -182,7 +191,6 @@ export const EditProfileForm: React.FC<ProfileEditorProps> = ({
const { value } = useProfileField({
field: key,
player,
owner: true,
});
return [key, value];
}),
@@ -427,7 +435,7 @@ export const EditProfileForm: React.FC<ProfileEditorProps> = ({
};
return (
<Stack as="form" onSubmit={handleSubmit(onSubmit)}>
<Stack as="form" onSubmit={handleSubmit(onSubmit)} maxW="full">
<Wrap>
<WrapItem flex={1} px={5}>
<FormControl isInvalid={errors.profileImageURL} align="center">
@@ -705,8 +713,8 @@ export const EditProfileForm: React.FC<ProfileEditorProps> = ({
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<ProfileEditorProps> = ({
</Box>
</FormControl>
</WrapItem>
<WrapItem flex={1} alignItems="center" px={5}>
<WrapItem flex={1} alignItems="center" px={5} minW="20rem">
<FormControl isInvalid={errors.timeZone}>
<Label htmlFor="name">Time Zone</Label>
<Controller

View File

@@ -13,14 +13,23 @@ import {
Text,
} from '@metafam/ds';
import { MetaLink } from 'components/Link';
import { XPSeedsBalance } from 'components/MegaMenu/XPSeedsBalance';
import { PlayerAvatar } from 'components/Player/PlayerAvatar';
import { useUser, useWeb3 } from 'lib/hooks';
import { useMounted, useProfileField, useUser, useWeb3 } from 'lib/hooks';
import React from 'react';
import { getPlayerName, getPlayerURL } from 'utils/playerHelpers';
import { XPSeedsBalance } from './XPSeedsBalance';
// Display player XP and Seed
export const MegaMenuFooter = () => {
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 (
<Flex
@@ -30,11 +39,11 @@ export const MegaMenuFooter = () => {
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 = () => {
<Menu>
<MenuButton
bg="transparent"
aria-label="menu options"
_focus={{ outline: 'none', bg: 'transparent' }}
_hover={{ bg: 'transparent' }}
_active={{ bg: 'transparent' }}
aria-label="Menu Options"
transition="filter 2.75s"
_focus={{ outline: 'none' }}
_hover={{ filter: 'brightness(1.1)' }}
_active={{ filter: 'hue-rotate(30deg)' }}
>
<Flex>
<PlayerAvatar player={user} w={14} h={14} m={0} />
@@ -57,7 +67,7 @@ export const MegaMenuFooter = () => {
p={0}
lineHeight={1}
>
{getPlayerName(user)}
{name}
</Text>
{user.rank && (
<Text fontSize={12} m={0} p={0} lineHeight={1}>
@@ -103,7 +113,7 @@ export const MegaMenuFooter = () => {
my={3.5}
px={8}
onClick={connect}
isLoading={connecting || fetching}
isLoading={!mounted || connecting || fetching}
>
Connect Wallet
</MetaButton>

View File

@@ -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 (
<Box w={{ base: 'fit-content', lg: '20%' }}>
<Box>
<MetaLink
href={link}
_focus={{ outline: 'none', bg: 'transparent' }}
@@ -56,6 +56,7 @@ export const MegaMenuHeader: React.FC = () => {
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 = () => {
>
<Flex
borderBottom="1px"
bg="rgba(0,0,0,0.5)"
bg="rgba(0, 0, 0, 0.5)"
borderColor="#2B2244"
sx={{ backdropFilter: 'blur(10px)' }}
px={4}
@@ -83,17 +84,17 @@ 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 ? (
<CloseIcon fontSize="1.5rem" color="#FFF" ml={2} />
<CloseIcon fontSize="2xl" color="#FFF" ml={2} />
) : (
<HamburgerIcon fontSize="2rem" color="#FFF" ml={2} />
<HamburgerIcon fontSize="3xl" color="#FFF" ml={2} />
)}
</Flex>
<Flex
@@ -104,22 +105,25 @@ export const MegaMenuHeader: React.FC = () => {
<Logo link={user ? '/dashboard' : '/'} />
<DesktopNavLinks />
{/* <Search /> */}
<Box
w="20%"
textAlign="right"
display={{ base: 'none', lg: 'block' }}
>
<Box textAlign="right" display={{ base: 'none', lg: 'block' }}>
{connected && !!user && !fetching && !connecting ? (
<DesktopPlayerStats player={user} />
) : (
<MetaButton
h={10}
px={6}
onClick={connect}
isLoading={connecting || fetching}
<Stack
fontWeight="bold"
fontFamily="Exo 2, san-serif"
align="center"
>
Connect
</MetaButton>
<MetaButton
h={10}
px={12}
onClick={connect}
isLoading={!mounted || connecting || fetching}
>
Connect
</MetaButton>
<Text color="red"> Mainnet Required</Text>
</Stack>
)}
</Box>
</Flex>

View File

@@ -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<Props> = ({
totalXP,
mobile = false,
}) => {
export const XPSeedsBalance: React.FC<Props> = ({ totalXP }) => {
const { pSeedBalance } = usePSeedBalance();
return (
<HStack flexDirection="row">
<Flex direction={['column', 'row']}>
<Tooltip label="Total XP" hasArrow>
<HStack
bg="rgba(0,0,0,0.25)"
border="1px solid #2B2244"
borderRadius="1rem"
bg="#00000044"
border={`1px solid ${MetaTheme.colors.purple[700]}`}
borderRadius="3xl"
px={4}
py={1}
minW="fit-content"
@@ -34,12 +31,14 @@ export const XPSeedsBalance: React.FC<Props> = ({
src={XPStar}
alignSelf="center"
alt="XP"
boxSize={mobile ? '1.5rem' : '1rem'}
boxSize={['1.5rem', '1rem']}
/>
<Text
w="full"
textAlign="right"
color="#FFF"
lineHeight={2}
fontSize={mobile ? 'sm' : 'xs'}
fontSize={['sm', 'xs']}
fontWeight="bold"
>
{Math.trunc(totalXP).toLocaleString()}
@@ -48,9 +47,9 @@ export const XPSeedsBalance: React.FC<Props> = ({
</Tooltip>
<Tooltip label="pSEEDs" hasArrow>
<HStack
bg="rgba(0,0,0,0.25)"
border="1px solid #2B2244"
borderRadius="1rem"
bg="#00000044"
border={`1px solid ${MetaTheme.colors.purple[700]}`}
borderRadius="3xl"
px={4}
py={1}
minW="fit-content"
@@ -59,12 +58,14 @@ export const XPSeedsBalance: React.FC<Props> = ({
src={SeedMarket}
alignSelf="center"
alt="Seed"
boxSize={mobile ? '1.5rem' : '1rem'}
boxSize={['1.5rem', '1rem']}
/>
<Text
w="full"
textAlign="right"
color="#FFF"
lineHeight={2}
fontSize={mobile ? 'sm' : 'xs'}
fontSize={['sm', 'xs']}
fontWeight="bold"
>
{parseInt(
@@ -74,6 +75,6 @@ export const XPSeedsBalance: React.FC<Props> = ({
</Text>
</HStack>
</Tooltip>
</HStack>
</Flex>
);
};

View File

@@ -22,42 +22,48 @@ const maskImageStyle = ({ url }: { url: string }): Record<string, string> => ({
WebkitMaskRepeat: 'no-repeat',
});
export type ColorBarProps = ChakraProps & {
mask: Maybe<number>;
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<ColorBarProps> = ({
mask = null,
types = null,
loading = false,
...props
}: ChakraProps & {
mask: Maybe<number>;
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 (
<Text fontStyle="italic" textAlign="center">
Loading Personality Information
<Text mt="3rem !important" fontStyle="italic" textAlign="center">
{status}
</Text>
);
}
if (mask === null) {
return (
<Text fontStyle="italic" textAlign="center">
Colors have not yet been chosen.
</Text>
);
}
if (types[mask] == null) {
return (
<Text fontStyle="italic" textAlign="center">
Error Loading Information For Mask: {mask.toString(2).padStart(5, '0')}
</Text>
);
if (mask === null || types == null) {
return null; // unreachable; for typescript
}
type ImagesArgProps = {

View File

@@ -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<PlayerAvatarProps> = 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 = {

View File

@@ -18,7 +18,7 @@ export const PlayerContacts: React.FC<Props> = ({
}) => {
const [copied, handleCopy] = useCopyToClipboard();
return (
<Wrap>
<Wrap justify="center">
{player?.accounts?.map((acc) => {
switch (acc.type) {
case 'TWITTER': {

View File

@@ -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<LinkGuildProps> = ({
daoUrl,
daoURL,
guildname,
children,
}) => {
if (guildname != null) {
return <InternalGuildLink guildName={guildname} children={children} />;
return <InternalGuildLink guildName={guildname} {...{ children }} />;
}
if (daoUrl != null) {
return <DaoHausLink daoUrl={daoUrl} children={children} />;
if (daoURL != null) {
return <DaoHausLink {...{ daoURL, children }} />;
}
return <>{children}</>;
};
@@ -34,14 +34,24 @@ export const InternalGuildLink: React.FC<InternalGuildLinkProps> = ({
);
type DaoHausLinkProps = {
daoUrl: string | null;
daoURL: string | null;
};
export const DaoHausLink: React.FC<DaoHausLinkProps> = ({ daoUrl, children }) =>
daoUrl != null ? (
<Link _hover={{ textDecoration: 'none' }} href={daoUrl} isExternal>
{children}
</Link>
) : (
<>{children}</>
);
export const DaoHausLink: React.FC<DaoHausLinkProps & LinkProps> = ({
daoURL,
children,
_hover = {},
...props
}) => {
_hover.textDecoration = 'none'; // eslint-disable-line no-param-reassign
if (daoURL != null) {
return (
<Link href={daoURL} isExternal {...{ _hover, ...props }}>
{children}
</Link>
);
}
return <>{children}</>;
};

View File

@@ -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<Props> = ({
isOwnProfile,
canEdit,
editing,
}) => {
const [show, setShow] = React.useState(false);
const fakeData = [
@@ -25,9 +25,8 @@ export const PlayerAchievements: React.FC<Props> = ({
return (
<ProfileSection
title="Achievements"
isOwnProfile={isOwnProfile}
canEdit={canEdit}
boxType={BoxType.PLAYER_ACHIEVEMENTS}
{...{ isOwnProfile, editing }}
type={BoxTypes.PLAYER_ACHIEVEMENTS}
withoutBG
>
{(fakeData || []).slice(0, show ? 999 : 3).map((title) => (

View File

@@ -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<BoxType>;
onAddBox: (arg0: BoxType, arg1: BoxMetadata) => void;
};
export const PlayerAddSection = React.forwardRef<HTMLDivElement, Props>(
({ player, personalityInfo, boxList = [], onAddBox, ...props }, ref) => {
({ player, boxes = [], onAddBox, ...props }, ref) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [boxType, setBoxType] = useState<Maybe<BoxType>>(null);
const [boxMetadata, setBoxMetadata] = useState<BoxMetadata>({});
const [type, setType] = useState<Maybe<BoxType>>(null);
const [metadata, setMetadata] = useState<BoxMetadata>({});
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 (
<Flex
w="100%"
ref={ref}
w="full"
h="full"
direction="column"
h="100%"
boxShadow="md"
pos="relative"
{...{ ref }}
>
<Flex
bg="whiteAlpha.200"
@@ -76,13 +75,14 @@ export const PlayerAddSection = React.forwardRef<HTMLDivElement, Props>(
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
</Button>
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<Modal {...{ isOpen, onClose }}>
<ModalOverlay />
<ModalContent
maxW="50rem"
@@ -99,9 +99,7 @@ export const PlayerAddSection = React.forwardRef<HTMLDivElement, Props>(
p={4}
top={0}
right={0}
_focus={{
boxShadow: 'none',
}}
_focus={{ boxShadow: 'none' }}
/>
<ModalHeader color="white" fontSize="4xl" fontWeight="normal">
Add New Block
@@ -112,39 +110,42 @@ export const PlayerAddSection = React.forwardRef<HTMLDivElement, Props>(
color="white"
w={{ base: '100%', sm: '30rem' }}
maxW="30rem"
minH="30rem"
>
<Select
css={{
'&>option': {
backgroundColor: '#40347C',
borderBottom: '2px solid #962d22',
placeholder="Select a Type to Add…"
borderColor={MetaTheme.colors.whiteAlpha[800]}
onChange={selectBoxType}
sx={{
textTransform: 'capitalize',
'& > option': {
backgroundColor: MetaTheme.colors.purpleBoxLight,
},
'& > option[value=""]': {
fontStyle: 'italic',
opacity: 0.75,
},
}}
placeholder="Select a section"
borderColor="offwhite"
onChange={selectBoxType}
>
{boxList.length === 0 ? (
{boxes.length === 0 ? (
<option value="nothing" disabled>
No choice :/
</option>
) : (
boxList.sort().map((box) => (
<option value={box} key={box}>
{box.toUpperCase()}
boxes.map((box) => (
<option key={box} value={box}>
{box.replace(/-/g, ' ')}
</option>
))
)}
</Select>
{boxType === BoxType.EMBEDDED_URL && (
{type === BoxTypes.EMBEDDED_URL && (
<Input
bg="dark"
w="100%"
placeholder="Paste the URL of the content"
placeholder="Provide the URL of the content"
_placeholder={{ color: 'whiteAlpha.500' }}
onChange={({ target: { value: url } }) =>
setBoxMetadata({ url })
setMetadata({ url })
}
size="lg"
borderRadius={0}
@@ -153,7 +154,7 @@ export const PlayerAddSection = React.forwardRef<HTMLDivElement, Props>(
borderWidth="2px"
/>
)}
{boxType && (
{type && (
<Flex
w={{ base: '100%', sm: '30rem' }}
maxW="30rem"
@@ -162,12 +163,11 @@ export const PlayerAddSection = React.forwardRef<HTMLDivElement, Props>(
>
<PlayerSection
isOwnProfile={false}
canEdit={false}
editing={false}
{...{
boxType,
boxMetadata,
type,
metadata,
player,
personalityInfo,
}}
/>
</Flex>
@@ -179,7 +179,7 @@ export const PlayerAddSection = React.forwardRef<HTMLDivElement, Props>(
colorScheme="blue"
mr={3}
onClick={addSection}
isDisabled={!boxType}
isDisabled={!type}
>
Save Block
</Button>

View File

@@ -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<ColorDispositionProps> = ({
player,
types,
isOwnProfile,
canEdit,
editing = false,
}) => {
const { value: mask } = useProfileField<number>({
const {
value: mask,
owner: isOwnProfile,
fetching,
} = useProfileField<number>({
field: 'colorMask',
player,
owner: isOwnProfile,
});
const [types, setTypes] = useState<PersonalityInfo>(null);
useEffect(() => {
const getInfo = async () => {
setTypes(await getPersonalityInfo());
};
getInfo();
}, []);
return (
<ProfileSection
title="Color Disposition"
boxType={BoxType.PLAYER_COLOR_DISPOSITION}
type={BoxTypes.PLAYER_COLOR_DISPOSITION}
withoutBG
{...{ isOwnProfile, canEdit }}
modalTitle={false}
{...{ isOwnProfile, editing }}
>
{mask == null ? (
<Text fontStyle="italic" textAlign="center" mb={6}>
Unspecified
</Text>
) : (
<ColorBar {...{ mask, types }} />
<ColorBar {...{ mask, types }} loading={fetching} />
)}
</ProfileSection>
);

View File

@@ -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<Props> = ({
player,
isOwnProfile,
canEdit,
editing,
}) => {
const [quests, setQuests] = useState<Array<QuestCompletionFragment>>([]);
@@ -44,13 +52,12 @@ export const PlayerCompletedQuests: React.FC<Props> = ({
return (
<ProfileSection
title="Completed Quests"
{...{ isOwnProfile, canEdit }}
boxType={BoxType.PLAYER_ACHIEVEMENTS}
{...{ isOwnProfile, editing }}
type={BoxTypes.PLAYER_ACHIEVEMENTS}
modalTitle={`Completed Quests (${quests.length})`}
modalText={quests.length ? 'Show All' : ''}
modal={<AllQuests quests={quests} />}
subheader='A quest is considered "complete" when it is accepted by the
quest owner.'
modalPrompt={quests.length ? 'Show All' : undefined}
modal={<AllQuests {...{ quests }} />}
subheader="A quest is considered complete when it is accepted by the quest owner."
>
{quests.length ? (
<Stack>
@@ -67,13 +74,16 @@ export const PlayerCompletedQuests: React.FC<Props> = ({
interface QuestProps {
quests: Array<QuestCompletionFragment>;
mb?: number;
}
const QuestList: React.FC<QuestProps> = ({ quests, mb = 2 }) => (
const QuestList: React.FC<QuestProps & BoxProps> = ({
quests,
mb = 2,
...props
}) => (
<>
{quests.map((quest) => (
<Box mb={mb}>
<Box {...{ mb, ...props }}>
<Link href={`/quest/${quest.questId}`} color="white">
<Text fontSize="xl">{quest.completed?.title}</Text>
</Link>

View File

@@ -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,
}) => (
<Link
href={nft.openseaLink}
isExternal
mb={noMargin ? undefined : 6}
display="flex"
>
const GalleryItem: React.FC<{ nft: Collectible }> = ({ nft }) => (
<Link href={nft.openseaLink} isExternal display="flex">
<Box
bgImage={`url(${nft.imageURL})`}
backgroundSize="contain"
@@ -39,131 +33,174 @@ const GalleryItem: React.FC<{ nft: Collectible; noMargin?: boolean }> = ({
minW={28}
minH={28}
/>
<Flex direction="column" ml={3} justify="center">
<Heading
fontSize="xs"
my={3}
display="inline-block"
style={{ wordWrap: 'break-word', fontVariant: 'small-caps' }}
<Tooltip label={nft.title} hasArrow>
<Flex
display="inline-grid"
direction="column"
ml={3}
h="full"
alignContent="center"
>
{nft.title}
</Heading>
<Text fontSize="sm">{nft.priceString}</Text>
</Flex>
<Heading
fontSize="xs"
ml="1em"
sx={{
textIndent: '-1em',
wordBreak: 'break-word',
fontVariant: 'small-caps',
}}
// ellipses look nice, but only allow one line, I think
// whiteSpace="nowrap"
// textOverflow="ellipsis"
// overflowX="hidden"
>
{nft.title}
</Heading>
<Text fontSize="sm">{nft.priceString}</Text>
</Flex>
</Tooltip>
</Link>
);
type GalleryModalProps = {
isOpen: boolean;
onClose: () => void;
nfts: Array<Collectible>;
};
const GalleryModal: React.FC<GalleryModalProps> = ({
isOpen,
onClose,
nfts,
}) => (
<Modal {...{ isOpen, onClose }} isCentered scrollBehavior="inside">
<ModalOverlay>
<ModalContent
mx={4}
maxW="6xl"
bgImage={`url(${BackgroundImage})`}
bgSize="cover"
bgAttachment="fixed"
>
<Box bg="purple80" borderTopRadius="lg" p={4} w="full">
<HStack>
<Text
fontSize="sm"
fontWeight="bold"
color="blueLight"
as="div"
mr="auto"
>
NFT Gallery
</Text>
<ModalCloseButton color="blueLight" />
</HStack>
</Box>
<Flex p={2}>
<Box
overflowY="auto"
overflowX="hidden"
maxH="calc(100vh - 12rem)"
borderBottomRadius="lg"
w="full"
sx={{
scrollbarColor: 'rgba(70, 20, 100, 0.8) #FFFFFF00',
'::-webkit-scrollbar': {
width: '0.5rem',
background: 'none',
},
'::-webkit-scrollbar-thumb': {
background: 'rgba(70, 20, 100, 0.8)',
borderRadius: '999px',
},
}}
>
<SimpleGrid
columns={{ base: 1, md: 2, lg: 3 }}
gap={6}
p={6}
boxShadow="md"
>
{nfts?.map((nft) => (
<GalleryItem
{...{ nft }}
key={`${nft.tokenId}-${nft.address}`}
/>
))}
</SimpleGrid>
</Box>
</Flex>
</ModalContent>
</ModalOverlay>
</Modal>
);
type Props = {
player: Player;
isOwnProfile?: boolean;
canEdit?: boolean;
editing?: boolean;
};
export const PlayerGallery: React.FC<Props> = ({
player,
isOwnProfile,
canEdit,
editing,
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { favorites, data, loading } = useOpenSeaCollectibles({ player });
const { favorites, data: nfts, loading, error } = useOpenSeaCollectibles({
player,
});
return (
<ProfileSection
title="NFT Gallery"
isOwnProfile={isOwnProfile}
canEdit={canEdit}
boxType={BoxType.PLAYER_NFT_GALLERY}
{...{ isOwnProfile, editing }}
type={BoxTypes.PLAYER_NFT_GALLERY}
withoutBG
>
{loading && <LoadingState mb={6} />}
{!loading &&
favorites?.map((nft) => <GalleryItem nft={nft} key={nft.tokenId} />)}
{!loading && data.length === 0 && (
<Text textAlign="center" fontStyle="italic" mb="1rem">
No{' '}
<Text as="span" title="Non-Fungible Token" borderBottom="2px dotted">
NFT
</Text>
s found for {isOwnProfile ? 'you' : 'this player'}.
</Text>
)}
{!loading && data?.length > 3 && (
<Text
as="span"
fontSize="xs"
color="cyanText"
cursor="pointer"
onClick={onOpen}
>
View all
</Text>
)}
<Modal
isOpen={isOpen}
onClose={onClose}
isCentered
scrollBehavior="inside"
>
<ModalOverlay>
<ModalContent
mx="1rem"
maxW="6xl"
bgImage={`url(${BackgroundImage})`}
bgSize="cover"
bgAttachment="fixed"
>
<Box bg="purple80" borderTopRadius="lg" p={4} w="100%">
<HStack>
<Text
fontSize="sm"
fontWeight="bold"
color="blueLight"
as="div"
mr="auto"
>
NFT Gallery
</Text>
<ModalCloseButton color="blueLight" />
</HStack>
</Box>
<Flex p={2}>
<Box
overflowY="scroll"
overflowX="hidden"
maxH="80vh"
borderBottomRadius="lg"
w="100%"
css={{
scrollbarColor: 'rgba(70,20,100,0.8) rgba(255,255,255,0)',
'::-webkit-scrollbar': {
width: '8px',
background: 'none',
},
'::-webkit-scrollbar-thumb': {
background: 'rgba(70,20,100,0.8)',
borderRadius: '999px',
},
}}
{(() => {
if (loading) {
return <LoadingState mb={6} />;
}
if (error) {
return (
<Text textAlign="center" fontStyle="italic" mb={4} color="red">
Error: {error}
</Text>
);
}
if (nfts.length === 0) {
return (
<Text textAlign="center" fontStyle="italic" mb={4}>
No{' '}
<Text
as="span"
title="Non-Fungible Token"
borderBottom="2px dotted"
>
<SimpleGrid
columns={{ base: 1, md: 2, lg: 3 }}
gap={6}
padding={6}
boxShadow="md"
>
{data?.map((nft) => (
<GalleryItem
nft={nft}
key={`${nft.tokenId}-${nft.address}`}
noMargin
/>
))}
</SimpleGrid>
NFT
</Text>
s found for {isOwnProfile ? 'you' : 'this player'}.
</Text>
);
}
return (
<>
<SimpleGrid columns={1}>
{favorites?.map((nft) => (
<GalleryItem {...{ nft }} key={nft.tokenId} />
))}
</SimpleGrid>
{nfts.length > 3 && (
<Box textAlign="end">
<GalleryModal {...{ isOpen, onClose, nfts }} />
<ViewAllButton onClick={onOpen} size={nfts.length} />
</Box>
</Flex>
</ModalContent>
</ModalOverlay>
</Modal>
)}
</>
);
})()}
</ProfileSection>
);
};

View File

@@ -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<Player> };
type TimeZoneDisplayProps = {
person?: Maybe<Player>;
};
type ColorDispositionProps = {
person?: Maybe<Player>;
personalityInfo: PersonalityInfo;
type DisplayComponentProps = {
player?: Maybe<Player>;
Wrapper?: React.FC;
};
export const PlayerHero: React.FC<Props> = ({
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<string>();
export const PlayerHero: React.FC<HeroProps> = ({ 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 (
<ProfileSection canEdit={canEdit} boxType={BoxType.PLAYER_HERO} withoutBG>
{isOwnProfile && !canEdit && (
<Box pos="absolute" right={5} top={5}>
<ProfileSection
{...{ editing }}
type={BoxTypes.PLAYER_HERO}
title={false}
withoutBG
p={[4, 2]}
>
{isOwnProfile && !editing && (
<Box pos="absolute" right={[0, 4]} top={[0, 4]}>
<IconButton
_focus={{ boxShadow: 'none' }}
variant="outline"
@@ -95,90 +77,45 @@ export const PlayerHero: React.FC<Props> = ({
isRound
_active={{
transform: 'scale(0.8)',
backgroundColor: 'transparent',
bg: 'transparent',
}}
/>
</Box>
)}
<Box textAlign="center" mb={8} mt={2}>
<Box align="center" mb={8} mt={2}>
<PlayerAvatar
w={{ base: 32, md: 56 }}
h={{ base: 32, md: 56 }}
w="min(var(--chakra-sizes-56), 100%)"
h="min(var(--chakra-sizes-56), 100%)"
{...{ player }}
/>
</Box>
<VStack spacing={6}>
<Box textAlign="center" maxW="full">
<Text
fontSize="xl"
fontFamily="heading"
mb={1}
textOverflow="ellipsis"
whiteSpace="nowrap"
overflowX="hidden"
title={playerName}
>
{playerName}
</Text>
</Box>
<Box w="100%">
{description && (
<Box align="flexStart" w="100%">
<PlayerHeroTile title="Bio">
<Text
fontSize={{ base: 'sm', sm: 'md' }}
textAlign="justify"
whiteSpace="pre-wrap"
>
{show
? description
: `${description.substring(0, MAX_BIO_LENGTH - 9)}`}
{description.length > MAX_BIO_LENGTH && (
<Text
as="span"
fontSize="xs"
color="cyanText"
cursor="pointer"
onClick={() => setShow((s) => !s)}
pl={1}
>
Read {show ? 'less' : 'more'}
</Text>
)}
</Text>
</PlayerHeroTile>
</Box>
)}
<Name {...{ player }} />
</Box>
<Description {...{ player }} />
<HStack mt={2}>
<PlayerContacts {...{ player }} />
<Contacts {...{ player }} />
</HStack>
{person?.profile?.pronouns && <PlayerPronouns {...{ person }} />}
{/* <PlayerHeroTile title="Website">
{/*
<PlayerHeroTile title="Website">
<Text>www.mycoolportfolio.com</Text>
</PlayerHeroTile> */}
</PlayerHeroTile>
*/}
<Wrap justify="space-between" w="full">
<WrapItem>
<PlayerHeroTile title="Availability">
<Availability {...{ person }} />
</PlayerHeroTile>
</WrapItem>
<WrapItem>
<PlayerHeroTile title="Time Zone">
<TimeZoneDisplay {...{ person }} />
</PlayerHeroTile>
</WrapItem>
{player?.profile?.emoji && (
<WrapItem>
<PlayerHeroTile title="Favorite Emoji">
<Text fontSize="1.25rem">{player.profile.emoji}</Text>
</PlayerHeroTile>
</WrapItem>
)}
</Wrap>
<Grid
templateColumns="repeat(auto-fill, minmax(9rem, 1fr))"
w="full"
rowGap={5}
>
<Pronouns {...{ player }} />
<Availability {...{ player }} />
<TimeZone {...{ player }} />
<Emoji {...{ player }} />
</Grid>
{/* <SimpleGrid columns={2} gap={6} width="full">
<PlayerHeroTile title="Country">
@@ -197,118 +134,251 @@ export const PlayerHero: React.FC<Props> = ({
</Flex>
</PlayerHeroTile>
</SimpleGrid> */}
{/* player?.profile?.colorMask && (
<PlayerHeroTile title="Color Disposition">
<ColorDispositionDisplay {...{ person, personalityInfo }} />
</PlayerHeroTile>
) */}
</VStack>
<Modal {...{ isOpen, onClose }}>
<ModalOverlay />
<ModalContent
maxW={['100%', 'min(80%, 60rem)']}
backgroundImage={`url(${BackgroundImage})`}
bgSize="cover"
bgAttachment="fixed"
p={[0, 8, 12]}
>
<ModalHeader
color="white"
fontSize="4xl"
alignSelf="center"
fontWeight="normal"
{isOwnProfile && (
<Modal {...{ isOpen, onClose }}>
<ModalOverlay />
<ModalContent
maxW={['100%', 'min(80%, 60rem)']}
backgroundImage={`url(${BackgroundImage})`}
bgSize="cover"
bgAttachment="fixed"
p={[0, 8, 12]}
>
Edit Profile
</ModalHeader>
<ModalCloseButton
color="pinkShadeOne"
size="xl"
p={{ base: 1, sm: 4 }}
_focus={{
boxShadow: 'none',
}}
/>
<ModalBody p={[0, 2]}>
<EditProfileForm player={user} {...{ onClose }} />
</ModalBody>
</ModalContent>
</Modal>
<ModalHeader
color="white"
fontSize="4xl"
alignSelf="center"
fontWeight="normal"
>
Edit Profile
</ModalHeader>
<ModalCloseButton
color="pinkShadeOne"
size="xl"
p={{ base: 1, sm: 4 }}
_focus={{
boxShadow: 'none',
}}
/>
<ModalBody p={[0, 2]}>
<EditProfileForm {...{ player, onClose }} />
</ModalBody>
</ModalContent>
</Modal>
)}
</ProfileSection>
);
};
const Availability: React.FC<AvailabilityProps> = ({ person }) => {
const [hours, setHours] = useState<number | null>(
person?.profile?.availableHours ?? null,
);
const updateFN = () => setHours(person?.profile?.availableHours ?? null);
const { animation } = useAnimateProfileChanges(
person?.profile?.availableHours,
updateFN,
);
export const Pronouns: React.FC<DisplayComponentProps> = ({
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 (
<Flex alignItems="center">
<Box pr={2}>
<FaClock color="blueLight" />
</Box>
<FlexContainer
align="stretch"
transition=" opacity 0.4s"
opacity={animation === 'fadeIn' ? 1 : 0}
>
<Text fontSize={{ base: 'md', sm: 'lg' }} pr={2}>
{hours == null ? (
<Text as="em">Unspecified</Text>
) : (
<>
<Text as="span" mr={0.5}>
{hours}
</Text>
<Text as="span" title="hours per week">
<Text as="sup">hr</Text><Text as="sub">week</Text>
</Text>
</>
)}
</Text>
</FlexContainer>
</Flex>
<Wrapper>
<PlayerHeroTile title="Personal Pronouns">
<MetaTag size="md" fontWeight="normal" backgroundColor="gray.600">
{pronouns}
</MetaTag>
</PlayerHeroTile>
</Wrapper>
);
};
const TimeZoneDisplay: React.FC<TimeZoneDisplayProps> = ({ person }) => {
const tz = getTimeZoneFor({ title: person?.profile?.timeZone });
const [timeZone, setTimeZone] = useState<string | null>(
tz?.abbreviation ?? null,
);
const [offset, setOffset] = useState<string>(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<DisplayComponentProps> = ({
player,
Wrapper = React.Fragment,
}) => {
const { emoji } = useProfileField({
field: 'emoji',
player,
});
if (!emoji || emoji === '') {
return null;
}
return (
<Flex alignItems="center">
<FlexContainer
align="stretch"
transition=" opacity 0.4s"
opacity={animation === 'fadeIn' ? 1 : 0}
<Wrapper>
<PlayerHeroTile title="Favorite Emoji">
<Text ml={10} mt={0} fontSize={45} lineHeight={0.75}>
{emoji}
</Text>
</PlayerHeroTile>
</Wrapper>
);
};
const Description: React.FC<DisplayComponentProps> = ({
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 (
<Wrapper>
<PlayerHeroTile title="Bio" align="flexStart">
<Text
fontSize={{ base: 'sm', sm: 'md' }}
textAlign="justify"
whiteSpace="pre-wrap"
>
{show || description.length <= MAX_BIO_LENGTH
? description
: `${description.substring(0, MAX_BIO_LENGTH - 9)}`}
{description.length > MAX_BIO_LENGTH && (
<Text
as="span"
fontSize="xs"
color="cyanText"
cursor="pointer"
onClick={() => setShow((s) => !s)}
px={0.5}
ml={2}
bg="#FFFFFF22"
border="1px solid #FFFFFF99"
borderRadius="15%"
_hover={{ bg: '#FFFFFF44' }}
>
Read {show ? 'Less' : 'More'}
</Text>
)}
</Text>
</PlayerHeroTile>
</Wrapper>
);
};
const Name: React.FC<DisplayComponentProps> = ({
player,
Wrapper = React.Fragment,
}) => {
const { name } = useProfileField({
field: 'name',
player,
getter: getPlayerName,
});
return (
<Wrapper>
<Text
fontSize="xl"
fontFamily="heading"
mb={1}
textOverflow="ellipsis"
whiteSpace="nowrap"
overflowX="hidden"
title={name ?? undefined}
>
{name}
</Text>
</Wrapper>
);
};
const Availability: React.FC<DisplayComponentProps> = ({
player,
Wrapper = React.Fragment,
}) => {
const { value: current } = useProfileField<number>({
field: 'availableHours',
player,
});
const [hours, setHours] = useState<Maybe<number>>(current);
const updateFN = () => setHours(current ?? null);
const { animation } = useAnimateProfileChanges(current, updateFN);
return (
<Wrapper>
<PlayerHeroTile title="Availability">
<Flex alignItems="center">
<Box pr={2}>
<FaClock color="blueLight" />
</Box>
<FlexContainer
align="stretch"
transition="opacity 0.4s"
opacity={animation === 'fadeIn' ? 1 : 0}
>
<Text fontSize={['md', 'lg']} pr={2}>
{hours == null ? (
<Text as="em">Unspecified</Text>
) : (
<>
<Text as="span" mr={0.5}>
{hours}
</Text>
<Text as="span" title="hours per week">
<Text as="sup">hr</Text><Text as="sub">week</Text>
</Text>
</>
)}
</Text>
</FlexContainer>
</Flex>
</PlayerHeroTile>
</Wrapper>
);
};
const TimeZone: React.FC<DisplayComponentProps> = ({
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 (
<Wrapper>
<PlayerHeroTile title="Time Zone">
<Flex align="center" whiteSpace="pre">
<Box pr={2}>
<FaGlobe color="blueLight" />
</Box>
{timeZone === null ? (
<Text fontStyle="italic">Unspecified</Text>
<Text fontStyle="italic" fontSize={['md', 'lg']}>
Unspecified
</Text>
) : (
<Tooltip label={tz?.name} hasArrow>
<Wrap justify="center" align="center">
<Wrap justify="center" align="center" userSelect="none">
<WrapItem my="0 !important">
<Text
fontSize={{ base: 'md', sm: 'lg' }}
fontSize={['md', 'lg']}
pr={1}
overflowX="hidden"
textOverflow="ellipsis"
@@ -327,51 +397,7 @@ const TimeZoneDisplay: React.FC<TimeZoneDisplayProps> = ({ person }) => {
</Tooltip>
)}
</Flex>
</FlexContainer>
</Flex>
);
};
export const ColorDispositionDisplay: React.FC<ColorDispositionProps> = ({
person,
personalityInfo: types,
}) => {
const [mask, setMask] = useState<number | null>(
person?.profile?.colorMask ?? null,
);
const updateFN = () => setMask(mask);
const { animation } = useAnimateProfileChanges(mask, updateFN);
return (
<FlexContainer
align="stretch"
justify="stretch"
w="100%"
transition=" opacity 0.4s"
opacity={animation === 'fadeIn' ? 1 : 0}
mb={-12}
>
<Flex align="center" whiteSpace="pre" w="100%">
{mask == null ? (
<Text fontStyle="italic" textAlign="center" mb={6}>
Unspecified
</Text>
) : (
<Link
isExternal
href={`//dysbulic.github.io/5-color-radar/#/combos/${mask
.toString(2)
.padStart(5, '0')}`}
w="100%"
fontSize={{ base: 'md', sm: 'lg' }}
fontWeight={600}
_focus={{ border: 'none' }}
>
<ColorBar {...{ mask, types }} />
</Link>
)}
</Flex>
</FlexContainer>
</PlayerHeroTile>
</Wrapper>
);
};

View File

@@ -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<Props & BoxProps> = ({
@@ -11,7 +13,7 @@ export const PlayerHeroTile: React.FC<Props & BoxProps> = ({
title,
...props
}) => (
<Box width="full" {...props}>
<Box w="full" {...props}>
<Text fontSize="md" color="blueLight" mb={1} whiteSpace="nowrap">
{title}
</Text>

View File

@@ -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<DaoListingProps> = ({ membership }) => {
const {
const DAOListing: React.FC<DAOListingProps> = ({
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 (
<chakra.span
textAlign={['center', 'left']}
display={['flex', 'inline']}
flexDirection={['column', 'inherit']}
>
<chakra.span mr={[0, 1]} _after={{ content: [undefined, '":"'] }}>
Shares
</chakra.span>
<chakra.span whiteSpace="nowrap" title={`${percent}%`}>
<Text as="sup">
{member != null ? member.toLocaleString() : 'Unknown'}
</Text>{' '}
<chakra.span fontSize="lg" pos="relative" top={0.5}>
</chakra.span>{' '}
<Text as="sub">{dao.toLocaleString()}</Text>
</chakra.span>
</chakra.span>
);
}
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 (
<LinkGuild {...{ daoUrl, guildname }}>
<HStack alignItems="center" mb={6}>
<Flex bg="purpleBoxLight" minW={16} minH={16} mr={6} borderRadius={8}>
{logoUrl ? (
<Image
src={logoUrl}
w="3.25rem"
h="3.25rem"
m="auto"
borderRadius={4}
/>
) : (
<ChainIcon chain={chain} boxSize={16} p={2} />
)}
<LinkGuild {...{ daoURL, guildname }}>
<Flex align="center" mb={4} p={2} direction={['column', 'row']}>
<Flex w="full" align="center" justifyContent={['space-around', 'end']}>
<Box bg="purpleBoxLight" minW={16} h={16} borderRadius={8}>
{logoURL ? (
<Image
src={logoURL}
w={14}
h={14}
mx="auto"
my={1}
borderRadius={4}
/>
) : (
<ChainIcon {...{ chain }} boxSize={16} p={2} />
)}
</Box>
<ChainIcon {...{ chain }} mx={2} boxSize="1.5em" />
</Flex>
<Box>
<Flex
w="full"
direction={['row', 'column']}
align={['center', 'start']}
>
<Heading
fontWeight="bold"
style={{ fontVariant: 'small-caps' }}
fontSize="xs"
color={daoUrl ? 'cyanText' : 'white'}
mb={1}
color={daoURL ? 'cyanText' : 'white'}
ml={[0, '1em']}
sx={{ textIndent: [0, '-1em'] }}
textAlign={['center', 'left']}
flexGrow={1}
>
<Center justifyContent="left">
{title ?? (
<Text>
Unknown{' '}
<Text as="span" textTransform="capitalize">
{chain}
</Text>{' '}
DAO
</Text>
)}
<ChainIcon chain={chain} ml={2} boxSize={3} />
</Center>
{title ?? (
<Text as="span">
Unknown{' '}
<Text as="span" textTransform="capitalize">
{chain}
</Text>{' '}
DAO
</Text>
)}
</Heading>
<HStack alignItems="center">
<Flex align="center" mt="0 !important">
{memberRank && (
<Text fontSize="xs" casing="capitalize" mr={3}>
{memberRank}
</Text>
)}
<Text fontSize="xs">{stake}</Text>
</HStack>
</Box>
</HStack>
<Text fontSize="xs" ml={[1.5, 0]}>
{stake}
</Text>
</Flex>
</Flex>
</Flex>
</LinkGuild>
);
};
type MembershipListProps = {
isOpen: boolean;
onClose: () => void;
memberships: Array<GuildMembership>;
};
const MembershipListModal: React.FC<MembershipListProps> = ({
isOpen,
onClose,
memberships,
}) => (
<Modal {...{ isOpen, onClose }} isCentered scrollBehavior="inside">
<ModalOverlay />
<ModalContent
maxW="min(var(--chakra-sizes-6xl), calc(100vw - 4rem))"
bgImage={`url(${BackgroundImage})`}
bgSize="cover"
bgAttachment="fixed"
maxH="full"
>
<Box bg="purple.800" borderTopRadius="lg" p={4} w="full">
<HStack>
<Text fontSize="sm" fontWeight="bold" color="blueLight" mr="auto">
Memberships
</Text>
<ModalCloseButton color="blueLight" />
</HStack>
</Box>
<Flex p={2}>
<Box
overflowY="auto"
overflowX="hidden"
maxH="calc(100vh - 10rem)"
borderBottomRadius="lg"
w="full"
color="white"
sx={{
scrollbarColor: 'rgba(70, 20, 100, 0.8) #FFFFFF00',
'::-webkit-scrollbar': {
width: '0.5rem',
background: 'none',
},
'::-webkit-scrollbar-thumb': {
background: 'rgba(70, 20, 100, 0.8)',
borderRadius: '999px',
},
}}
>
<SimpleGrid
columns={{ base: 1, md: 2, lg: 3, '2xl': 4 }}
gap={2}
p={4}
boxShadow="md"
justify="center"
>
{memberships.map((membership) => (
<DAOListing key={membership.memberId} {...{ membership }} />
))}
</SimpleGrid>
</Box>
</Flex>
</ModalContent>
</Modal>
);
type MembershipSectionProps = {
player: Player;
isOwnProfile?: boolean;
canEdit?: boolean;
editing?: boolean;
};
export const PlayerMemberships: React.FC<MembershipSectionProps> = ({
player,
isOwnProfile,
canEdit,
editing,
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [memberships, setMemberships] = useState<GuildMembership[]>([]);
@@ -129,98 +230,33 @@ export const PlayerMemberships: React.FC<MembershipSectionProps> = ({
return (
<ProfileSection
title="DAO Memberships"
boxType={BoxType.PLAYER_DAO_MEMBERSHIPS}
type={BoxTypes.PLAYER_DAO_MEMBERSHIPS}
withoutBG
{...{ isOwnProfile, canEdit }}
{...{ isOwnProfile, editing }}
>
{loading && <LoadingState mb={6} />}
{!loading && memberships.length === 0 && (
<Text fontStyle="italic" textAlign="center" mb="1rem">
<Text fontStyle="italic" textAlign="center" mb={4}>
No DAO member&shy;ships found for{' '}
{isOwnProfile ? 'you' : 'this player'}.
</Text>
)}
{memberships.slice(0, 4).map((membership) => (
<DaoListing key={membership.memberId} {...{ membership }} />
))}
<Wrap justify="center">
{memberships.slice(0, 4).map((membership) => (
<WrapItem key={membership.memberId}>
<DAOListing {...{ membership }} />
</WrapItem>
))}
</Wrap>
{memberships.length > 4 && (
<Text
as="span"
fontSize="xs"
color="cyanText"
cursor="pointer"
onClick={onOpen}
>
View All ({memberships.length})
</Text>
<Box textAlign="end">
<MembershipListModal {...{ isOpen, onClose, memberships }} />
<ViewAllButton onClick={onOpen} size={memberships.length} />
</Box>
)}
<Modal
isOpen={isOpen}
onClose={onClose}
isCentered
scrollBehavior="inside"
>
<ModalOverlay>
<ModalContent
maxW="6xl"
bgImage={`url(${BackgroundImage})`}
bgSize="cover"
bgAttachment="fixed"
>
<Box bg="purple80" borderTopRadius="lg" p={4} w="100%">
<HStack>
<Text
fontSize="sm"
fontWeight="bold"
color="blueLight"
as="div"
mr="auto"
>
Memberships
</Text>
<ModalCloseButton color="blueLight" />
</HStack>
</Box>
<Flex p={2}>
<Box
overflowY="scroll"
overflowX="hidden"
maxH="80vh"
borderBottomRadius="lg"
w="100%"
color="white"
css={{
scrollbarColor: 'rgba(70,20,100,0.8) rgba(255,255,255,0)',
'::-webkit-scrollbar': {
width: '8px',
background: 'none',
},
'::-webkit-scrollbar-thumb': {
background: 'rgba(70,20,100,0.8)',
borderRadius: '999px',
},
}}
>
<SimpleGrid
columns={{ base: 1, md: 2 }}
gap={6}
padding={6}
boxShadow="md"
>
{memberships.map((membership) => (
<DaoListing key={membership.memberId} {...{ membership }} />
))}
</SimpleGrid>
</Box>
</Flex>
</ModalContent>
</ModalOverlay>
</Modal>
</ProfileSection>
);
};

View File

@@ -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<Props> = ({ person }) => {
const [pronouns, setPronouns] = useState<string>(
person?.profile?.pronouns ?? '',
);
const updateFN = () => {
setPronouns(person?.profile?.pronouns ?? '');
};
const { animation } = useAnimateProfileChanges(
person?.profile?.pronouns,
updateFN,
);
return pronouns ? (
<PlayerHeroTile title="Personal Pronouns">
<FlexContainer
direction="row"
justify="flex-start"
transition=" opacity 0.4s"
opacity={animation === 'fadeIn' ? 1 : 0}
>
<MetaTag size="md" fontWeight="normal" backgroundColor="gray.600">
{pronouns}
</MetaTag>
</FlexContainer>
</PlayerHeroTile>
) : (
<></>
);
};

View File

@@ -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<Props> = ({
player,
isOwnProfile,
canEdit,
}) => (
<ProfileSection
title="Roles"
boxType={BoxType.PLAYER_ROLES}
withoutBG
{...{ 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 (
<ProfileSection
title="Roles"
modalTitle={false}
type={BoxTypes.PLAYER_ROLES}
withoutBG
{...{ isOwnProfile, editing }}
>
{roles?.length === 0 && (
<Text fontStyle="italic" textAlign="center" mb="1rem">
No Roles found for {isOwnProfile ? 'you' : 'this player'}.
No roles assigned to {isOwnProfile ? 'you' : 'this player'}.
</Text>
))}
<Wrap mb="1rem">
{player.roles &&
player.roles
.sort((a, b) => (a.rank > b.rank ? 1 : -1))
.map(({ role, rank, PlayerRole }) => (
<MetaTag key={role}>
)}
<Wrap mb={4} justify="center">
{(roles ?? []).map((role, rank) => (
<WrapItem key={role}>
<MetaTag>
<BoxedNextImage
src={`/assets/roles/${role.toLowerCase()}.svg`}
alt={PlayerRole.label}
h="4"
w="4"
alt={role}
h={4}
w={4}
mr={2}
/>
<Text
@@ -45,10 +53,12 @@ export const PlayerRoles: React.FC<Props> = ({
casing="uppercase"
my={{ base: 0, md: 2 }}
>
{PlayerRole.label}
{role}
</Text>
</MetaTag>
))}
</Wrap>
</ProfileSection>
);
</WrapItem>
))}
</Wrap>
</ProfileSection>
);
};

View File

@@ -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<Props> = ({
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 (
<ProfileSection
title="Skills"
{...{ isOwnProfile, canEdit }}
boxType={BoxType.PLAYER_SKILLS}
modalTitle={false}
{...{ isOwnProfile, editing }}
type={BoxTypes.PLAYER_SKILLS}
withoutBG
>
{!player?.skills?.length ? (
{!skills?.length ? (
<Text fontStyle="italic" textAlign="center" mb={6}>
{isOwnProfile ? 'You havent ' : 'This player hasnt '}
defined any skills.
</Text>
) : (
<Wrap
transition="opacity 0.4s"
opacity={animation === 'fadeIn' ? 1 : 0}
justify="center"
>
{(playerSkills || []).map(({ id, name, category }) => (
<Wrap transition="opacity 0.4s" justify="center">
{(skills || []).map(({ id, name, category }) => (
<WrapItem key={id}>
<MetaTag
size="md"

View File

@@ -1,65 +1,51 @@
import { Text } from '@metafam/ds';
import { FlexContainer } from 'components/Container';
import { ProfileSection } from 'components/Profile/ProfileSection';
import { ExplorerType, Player } from 'graphql/autogen/types';
import { useAnimateProfileChanges } from 'lib/hooks/players';
import React, { useState } from 'react';
import { BoxType } from 'utils/boxTypes';
import { useProfileField } from 'lib/hooks';
import React from 'react';
import { BoxTypes } from 'utils/boxTypes';
type Props = {
player: Player;
isOwnProfile?: boolean;
canEdit?: boolean;
editing?: boolean;
};
export const PlayerType: React.FC<Props> = ({
player,
isOwnProfile,
canEdit,
}) => {
const [playerType, setPlayerType] = useState<ExplorerType | null>(
(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<Props> = ({ player, editing }) => {
const { explorerType, owner: isOwnProfile } = useProfileField<ExplorerType>({
field: 'explorerType',
player,
});
return (
<ProfileSection
title="Player type"
{...{ isOwnProfile, canEdit }}
boxType={BoxType.PLAYER_TYPE}
title="Player Type"
{...{ isOwnProfile, editing }}
type={BoxTypes.PLAYER_TYPE}
withoutBG
>
{!playerType ? (
{!explorerType ? (
<Text fontStyle="italic" textAlign="center" mb={6}>
Unspecified
</Text>
) : (
<FlexContainer
align="stretch"
transition=" opacity 0.4s"
opacity={animation === 'fadeIn' ? 1 : 0}
>
<>
<Text
color="white"
fontWeight="600"
fontWeight={600}
casing="uppercase"
fontSize={{ base: 'md', sm: 'lg' }}
>
{playerType.title}
{explorerType.title}
</Text>
<Text
fontSize={{ base: 'sm', sm: 'md' }}
color="blueLight"
textAlign="justify"
>
{playerType.description}
{explorerType.description}
</Text>
</FlexContainer>
</>
)}
</ProfileSection>
);

View File

@@ -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<LayoutItem>;
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<BoxType>;
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<BoxType, number>;
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<BoxType, Coordinates>;
const DEFAULT_BOX_POSITIONS: Record<ChakraSize, Positions> = {
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 = {

View File

@@ -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<EmbeddedUrlProps> = ({ url, canEdit }) => (
export const EmbeddedUrl: React.FC<EmbeddedUrlProps> = ({ url, editing }) => (
<ProfileSection
canEdit={canEdit}
boxType={BoxType.EMBEDDED_URL}
{...{ editing }}
type={BoxTypes.EMBEDDED_URL}
pb={0}
withoutBG
>
<LinkPreview {...{ url, canEdit }} />
<LinkPreview {...{ url, editing }} />
</ProfileSection>
);
interface LinkPreviewProps {
url?: string;
canEdit?: boolean;
editing?: boolean;
}
interface URIMetadata {
@@ -47,21 +47,22 @@ const LinkPreview: React.FC<LinkPreviewProps> = ({ url: inputUrl = '' }) => {
const [metadata, setMetadata] = useState<Maybe<URIMetadata>>(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<LinkPreviewProps> = ({ url: inputUrl = '' }) => {
</Box>
)}
<Box mt={1} color="cyanText" fontSize="sm">
{siteName && <span>{siteName} </span>}
{siteName && <Text as="span">{siteName} </Text>}
<Text isTruncated>{url}</Text>
</Box>
</Box>

View File

@@ -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<Props> = ({
boxMetadata,
boxType,
metadata,
type,
player,
isOwnProfile,
personalityInfo,
canEdit,
editing,
}) => {
switch (boxType) {
case BoxType.PLAYER_HERO:
return (
<PlayerHero {...{ player, personalityInfo, isOwnProfile, canEdit }} />
);
case BoxType.PLAYER_SKILLS:
return <PlayerSkills {...{ player, isOwnProfile, canEdit }} />;
case BoxType.PLAYER_NFT_GALLERY:
return <PlayerGallery {...{ player, isOwnProfile, canEdit }} />;
case BoxType.PLAYER_DAO_MEMBERSHIPS:
return <PlayerMemberships {...{ player, isOwnProfile, canEdit }} />;
case BoxType.PLAYER_COLOR_DISPOSITION:
return (
<PlayerColorDisposition
{...{ player, isOwnProfile, canEdit, types: personalityInfo }}
/>
);
case BoxType.PLAYER_TYPE:
return <PlayerType {...{ player, isOwnProfile, canEdit }} />;
case BoxType.PLAYER_ROLES:
return <PlayerRoles {...{ player, isOwnProfile, canEdit }} />;
case BoxType.PLAYER_ACHIEVEMENTS:
return <PlayerAchievements {...{ player, isOwnProfile, canEdit }} />;
case BoxType.PLAYER_COMPLETED_QUESTS:
return <PlayerCompletedQuests {...{ player, isOwnProfile, canEdit }} />;
case BoxType.EMBEDDED_URL: {
const url = boxMetadata?.url as string;
return url ? <EmbeddedUrl {...{ url, canEdit }} /> : <></>;
switch (type) {
case BoxTypes.PLAYER_HERO:
return <PlayerHero {...{ player, editing }} />;
case BoxTypes.PLAYER_SKILLS:
return <PlayerSkills {...{ player, isOwnProfile, editing }} />;
case BoxTypes.PLAYER_NFT_GALLERY:
return <PlayerGallery {...{ player, isOwnProfile, editing }} />;
case BoxTypes.PLAYER_DAO_MEMBERSHIPS:
return <PlayerMemberships {...{ player, isOwnProfile, editing }} />;
case BoxTypes.PLAYER_COLOR_DISPOSITION:
return <PlayerColorDisposition {...{ player, editing }} />;
case BoxTypes.PLAYER_TYPE:
return <PlayerType {...{ player, editing }} />;
case BoxTypes.PLAYER_ROLES:
return <PlayerRoles {...{ player, isOwnProfile, editing }} />;
case BoxTypes.PLAYER_ACHIEVEMENTS:
return <PlayerAchievements {...{ player, isOwnProfile, editing }} />;
case BoxTypes.PLAYER_COMPLETED_QUESTS:
return <PlayerCompletedQuests {...{ player, isOwnProfile, editing }} />;
case BoxTypes.EMBEDDED_URL: {
const { url } = metadata ?? {};
return url ? <EmbeddedUrl {...{ url, editing }} /> : null;
}
default:
return <></>;
return null;
}
};
export const PlayerSection = React.forwardRef<HTMLDivElement, Props>(
(
{
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 (
<Flex
w="100%"
ref={ref}
{...{ ref }}
direction="column"
h="auto"
minH="100%"
@@ -94,15 +74,14 @@ export const PlayerSection = React.forwardRef<HTMLDivElement, Props>(
>
<PlayerSectionInner
{...{
boxMetadata,
boxType,
metadata,
type,
player,
isOwnProfile,
canEdit,
personalityInfo,
editing,
}}
/>
{canEdit && (
{editing && (
<Flex
className="gridItemOverlay"
w="100%"
@@ -113,9 +92,9 @@ export const PlayerSection = React.forwardRef<HTMLDivElement, Props>(
left={0}
/>
)}
{canEdit && boxType && boxType !== BoxType.PLAYER_HERO && (
{editing && type && type !== BoxTypes.PLAYER_HERO && (
<IconButton
aria-label="Edit Profile Info"
aria-label="Remove Profile Section"
size="lg"
pos="absolute"
top={0}
@@ -124,7 +103,7 @@ export const PlayerSection = React.forwardRef<HTMLDivElement, Props>(
color="pinkShadeOne"
icon={<FaTimes />}
_hover={{ color: 'white' }}
onClick={() => onRemoveBox?.(boxKey)}
onClick={() => onRemoveBox?.(key)}
_focus={{
boxShadow: 'none',
backgroundColor: 'transparent',

View File

@@ -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<boolean>;
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<ProfileSectionProps> = ({
export const ProfileSection: React.FC<
ProfileSectionProps & Omit<FlexProps, 'title'>
> = ({
children,
isOwnProfile,
canEdit,
boxType,
editing,
type: boxType,
title,
withoutBG = false,
modalText,
modalPrompt,
modal,
modalTitle,
subheader,
@@ -55,129 +56,116 @@ export const ProfileSection: React.FC<ProfileSectionProps> = ({
return (
<Flex
minW="min(var(--chakra-sizes-72), calc(100vw - 3rem))"
minW="min(var(--chakra-sizes-72), calc(100vw - 3rem), 100%)"
pos="relative"
w="100%"
h="auto"
direction="column"
{...props}
>
{title && (
<Box bg="purpleProfileSection" borderTopRadius="lg" pt={5} pb={5}>
<HStack h={5} pr={4} pl={8}>
<Text
fontSize="md"
color="blueLight"
as="div"
mr="auto"
fontWeight={600}
casing="uppercase"
>
{title}
</Text>
{!modal && isOwnProfile && !canEdit && isBoxDataEditable(boxType) && (
{title !== false && (
<Box bg="purpleProfileSection" borderTopRadius="lg" py={5}>
<Flex h={5} pr={2} pl={6} align="center">
{title && (
<Text
fontSize="md"
color="blueLight"
mr="auto"
fontWeight={600}
casing="uppercase"
>
{title}
</Text>
)}
{isOwnProfile && !editing && isEditable(boxType) && (
<IconButton
aria-label="Edit Profile Info"
aria-label={`Edit ${title}`}
size="lg"
background="transparent"
color="pinkShadeOne"
icon={<EditIcon />}
_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 && (
<Button
color="pinkShadeOne"
background="transparent"
_hover={{ color: 'white' }}
_focus={{ boxShadow: 'none' }}
_active={{ transform: 'scale(0.8)' }}
onClick={onOpen}
_focus={{
boxShadow: 'none',
backgroundColor: 'transparent',
}}
_active={{
transform: 'scale(0.8)',
backgroundColor: 'transparent',
}}
>
{modalText}
{modalPrompt}
</Button>
)}
</HStack>
</Flex>
</Box>
)}
<Box
bg={withoutBG ? 'none' : 'blueProfileSection'}
borderBottomRadius="lg"
borderTopRadius={!title ? 'lg' : 0}
p={boxType === BoxType.EMBEDDED_URL ? 0 : 8}
boxShadow={withoutBG ? 'none' : 'md'}
css={{ backdropFilter: 'blur(8px)' }}
w="100%"
px={boxType === BoxTypes.EMBEDDED_URL ? 0 : [1, 8]}
py={boxType === BoxTypes.EMBEDDED_URL ? 0 : 8}
sx={{ backdropFilter: 'blur(8px)' }}
w="full"
pos="relative"
pointerEvents={canEdit ? 'none' : 'initial'}
pb={8}
{...props}
pointerEvents={editing ? 'none' : 'initial'}
>
{children}
</Box>
{boxType && (
<Modal isCentered scrollBehavior="inside" {...{ isOpen, onClose }}>
{(boxType || modal) && (
<Modal {...{ isOpen, onClose }}>
<ModalOverlay />
<ModalContent
maxW="80%"
maxH="80%"
maxW={['100%', '80%']}
backgroundImage={`url(${BackgroundImage})`}
bgSize="cover"
bgAttachment="fixed"
p={[4, 8, 12]}
>
<ModalHeader
color="white"
fontSize="4xl"
alignSelf="center"
fontWeight="normal"
textAlign="center"
>
{modalTitle || title}
{modalTitle !== false && (
<ModalHeader
color="white"
fontSize="4xl"
alignSelf="center"
fontWeight="normal"
textAlign="center"
>
{modalTitle ?? title}
{subheader && (
<Text
fontStyle="italic"
color="gray.400"
textAlign="center"
fontSize="md"
mt={3}
mb={10}
>
{subheader}
</Text>
)}
</ModalHeader>
{subheader && (
<Text
fontStyle="italic"
color="gray.400"
textAlign="center"
fontSize="md"
mt={3}
mb={10}
>
{subheader}
</Text>
)}
</ModalHeader>
)}
<ModalCloseButton
color="pinkShadeOne"
size="xl"
p={4}
_focus={{ boxShadow: 'none' }}
/>
{!modal && !modalText && (
<EditSectionBox {...{ boxType, onClose }} />
)}
{modalText && modal && <ModalBody>{modalText && modal}</ModalBody>}
<ModalBody p={[0, 6]}>
{modal ?? <EditSection {...{ boxType, onClose }} />}
</ModalBody>
{/* 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 && (
<ModalFooter mt={6} justifyContent="center">
{modal && (
<ModalFooter mt={6} justify="center">
<Button
variant="ghost"
onClick={onClose}
@@ -196,34 +184,36 @@ export const ProfileSection: React.FC<ProfileSectionProps> = ({
);
};
const isBoxDataEditable = (boxType?: Maybe<BoxType>) =>
!!boxType &&
[
BoxType.PLAYER_TYPE,
BoxType.PLAYER_COLOR_DISPOSITION,
BoxType.PLAYER_SKILLS,
BoxType.PLAYER_ROLES,
].includes(boxType);
const isEditable = (type?: Maybe<BoxType>) =>
!!type &&
([
BoxTypes.PLAYER_TYPE,
BoxTypes.PLAYER_COLOR_DISPOSITION,
BoxTypes.PLAYER_SKILLS,
BoxTypes.PLAYER_ROLES,
] as Array<BoxType>).includes(type);
const EditSectionBox = ({
const EditSection = ({
boxType,
onClose,
}: {
boxType: string;
boxType?: string;
onClose: () => void;
}) => {
const buttonLabel = 'Save';
switch (boxType) {
case BoxType.PLAYER_TYPE: {
return <SetupPlayerType isEdit {...{ onClose }} />;
case BoxTypes.PLAYER_TYPE: {
return <SetupPlayerType {...{ onClose, buttonLabel }} />;
}
case BoxType.PLAYER_COLOR_DISPOSITION: {
return <SetupPersonalityType isEdit {...{ onClose }} />;
case BoxTypes.PLAYER_COLOR_DISPOSITION: {
return <SetupColorDisposition {...{ onClose, buttonLabel }} />;
}
case BoxType.PLAYER_SKILLS: {
return <SetupSkills isEdit {...{ onClose }} />;
case BoxTypes.PLAYER_SKILLS: {
return <SetupSkills {...{ onClose, buttonLabel }} />;
}
case BoxType.PLAYER_ROLES: {
return <SetupRoles isEdit {...{ onClose }} />;
case BoxTypes.PLAYER_ROLES: {
return <SetupRoles {...{ onClose, buttonLabel }} />;
}
default:
}

View File

@@ -11,7 +11,6 @@ import {
import { MetaLink } from 'components/Link';
import { CompletionStatusTag } from 'components/Quest/QuestTags';
import {
Player,
Quest,
QuestCompletionStatus_ActionEnum,
QuestCompletionStatus_Enum,
@@ -92,8 +91,8 @@ export const QuestCompletions: React.FC<Props> = ({ quest }) => {
}) => (
<Box key={id} w="100%">
<HStack px={4} py={4}>
<Avatar name={getPlayerName(player as Player)} />
<CompletionStatusTag status={status} />
<Avatar name={getPlayerName(player)} />
<CompletionStatusTag {...{ status }} />
<Text>
<i>
by{' '}
@@ -101,7 +100,7 @@ export const QuestCompletions: React.FC<Props> = ({ quest }) => {
as={getPlayerURL(player)}
href="/player/[username]"
>
{getPlayerName(player as Player)}
{getPlayerName(player)}
</MetaLink>
</i>
</Text>

View File

@@ -71,11 +71,11 @@ export const Card: React.FC<CardProps> = ({ title, description, Content }) => {
bgImage={[ModalCardBg, ModalCardBg, BackgroundImage]}
bgPos="center"
bgColor="purpleModalDark"
bgRepeat="no-repeat"
bgSize="cover"
textColor="white"
maxH={['full', 'full', '90%']}
h={['full', 'full', 'auto']}
// w="auto"
w="full"
maxW="3xl"
alignItems="center"
>

View File

@@ -61,9 +61,13 @@ export const WTFisXP = () => (
a bunch of other granularly-adjustable parameters.
</Text>
<Text mb={2}>
<em>Wait, you leave all the value allocation to an algorithm? 😱</em>
</Text>
<UnorderedList>
<ListItem>
<Text mb={2}>
Wait, you leave all the value allocation to an algorithm? 😱
</Text>
</ListItem>
</UnorderedList>
<Text mb={2}>
Of course not! Although it <em>does</em> do a pretty damn good job, we
@@ -71,9 +75,11 @@ export const WTFisXP = () => (
properly accounted for.
</Text>
<Text mb={2}>
<em>Can it be used by other DAOs?</em>
</Text>
<UnorderedList>
<ListItem>
<Text mb={2}>Can it be used by other DAOs?</Text>
</ListItem>
</UnorderedList>
<Text mb={2}>
Of course! We believe its a good DAO bootstrapping/accounting/rewarding
@@ -365,7 +371,7 @@ export const BuyingAndSelling = () => (
<ul>
<ListItem ml={4}>
Seeds not showing up? Search by
0x30cf203b48edaa42c3b4918e955fed26cd012a3f
0xeaecc18198a475c921b24b8a6c1c1f0f5f3f7ea0
</ListItem>
</ul>
</ListItem>
@@ -376,7 +382,7 @@ export const BuyingAndSelling = () => (
<ListItem>
That's it! Now go to your MetaMask, scroll down to add custom token
&amp; paste the token address:
0x30cf203b48edaa42c3b4918e955fed26cd012a3f
0xeaecc18198a475c921b24b8a6c1c1f0f5f3f7ea0
</ListItem>
</UnorderedList>
<Text>If you want to sell your Seeds:</Text>
@@ -484,7 +490,7 @@ export const PlantingAndWatering = () => (
</Box>
<UnorderedList>
<ListItem>
Ideally, it would be WETH &amp;{' '}
Ideally, it would be WETH &amp;
<Link
ml={1}
href="https://reflexer.finance/"
@@ -767,7 +773,7 @@ export const FAQ = () => {
{isOpenCap && (
<Text>
At the time of writing this post (February 2022), the market cap was
$600k
~$600k
</Text>
)}
</div>

View File

@@ -1,192 +0,0 @@
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 { CeramicError } from 'lib/hooks';
import {
PropsWithChildren,
ReactElement,
useCallback,
useEffect,
useState,
} from 'react';
import { Control, useForm, UseFormRegisterReturn } from 'react-hook-form';
import { WizardPaneProps } from './ProfileWizardPane';
export type MaybeModalProps = {
onClose?: () => void;
};
export type GenericPaneProps<T = string> = WizardPaneProps & {
value: Maybe<T>;
fetching: boolean;
onSave?: ({
values,
setStatus,
}: {
values: Record<string, unknown>;
setStatus?: (msg: string) => void;
}) => void;
};
export type WizardPaneCallbackProps<T = string> = {
register: (
field: string,
opts: Record<string, unknown>,
) => UseFormRegisterReturn;
control: Control;
loading: boolean;
errored: boolean;
dirty: boolean;
current: T;
setter: (arg: T | ((prev: T) => T)) => void;
};
export const GenericWizardPane = <T,>({
field,
title,
prompt,
onClose,
onSave,
value: existing,
fetching = false,
children,
}: PropsWithChildren<GenericPaneProps<T>>) => {
const { onNextPress, nextButtonLabel } = useSetupFlow();
const [status, setStatus] = useState<Maybe<string | ReactElement>>();
const {
register,
control,
handleSubmit,
setValue,
watch,
formState: { errors, isValidating: validating },
} = useForm();
const current = watch(field, existing);
const toast = useToast();
useEffect(() => {
setValue(field, existing);
}, [existing, field, setValue]);
const onSubmit = useCallback(
async (values) => {
try {
if (current === existing) {
setStatus('No Change. Skipping Save…');
await new Promise((resolve) => {
setTimeout(resolve, 10);
});
} else if (onSave) {
setStatus('Saving…');
await onSave({ values, setStatus });
}
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);
}
},
[current, existing, onNextPress, onSave, toast],
);
const setter = useCallback(
(val: unknown) => {
let next = val;
if (val instanceof Function) {
next = val(current);
}
setValue(field, next);
},
[current, field, setValue],
);
return (
<FlexContainer as="form" onSubmit={handleSubmit(onSubmit)}>
{title && (
<MetaHeading mb={5} textAlign="center">
{title}
</MetaHeading>
)}
{typeof prompt === 'string' ? (
<Text mb={0} textAlign="center">
{prompt}
</Text>
) : (
prompt
)}
<FormControl isInvalid={!!errors[field]} isDisabled={fetching}>
{fetching && (
<Flex justify="center" align="center" my={8}>
<Spinner thickness="4px" speed="1.25s" size="lg" mr={4} />
<Text>Loading Current Value</Text>
</Flex>
)}
{validating && (
<Stack align="center" mb={4}>
<Spinner thickness="4px" speed="1.25s" size="lg" />
<Text>Validating</Text>
</Stack>
)}
<Box mt={10}>
{typeof children === 'function'
? children.call(null, {
register,
control,
loading: fetching,
errored: !!errors[field],
dirty: current !== existing,
current,
setter,
})
: children}
<FormErrorMessage style={{ justifyContent: 'center' }}>
{errors[field]?.message}
</FormErrorMessage>
</Box>
</FormControl>
<Wrap>
<WrapItem>
<StatusedSubmitButton label={nextButtonLabel} {...{ status }} />
</WrapItem>
{onClose && (
<WrapItem>
<Button
variant="ghost"
onClick={onClose}
color="white"
_hover={{ bg: '#FFFFFF11' }}
_active={{ bg: '#FF000011' }}
>
Close
</Button>
</WrapItem>
)}
</Wrap>
</FlexContainer>
);
};

View File

@@ -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<T = string> = {
register: (
field: string,
opts: Record<string, unknown>,
) => 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 = <T extends ProfileValueType>({
field,
title,
prompt,
onClose,
children,
...props
}: PropsWithChildren<WizardPaneProps>) => {
const { onNextPress, nextButtonLabel } = useSetupFlow();
const [status, setStatus] = useState<Maybe<string | ReactElement>>();
const { user } = useUser();
const { value: existing } = useProfileField<T>({
const { value, user } = useProfileField<T>({
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 (
<FlexContainer as="form" onSubmit={handleSubmit(onSubmit)}>
{title && (
<MetaHeading mb={5} textAlign="center">
{title}
</MetaHeading>
)}
{typeof prompt === 'string' ? (
<Text mb={10} textAlign="center">
{prompt}
</Text>
) : (
prompt
)}
<FormControl isInvalid={!!errors[field]} isDisabled={!user}>
{!user && (
<Flex justify="center" my={4}>
<Spinner thickness="4px" speed="1.25s" size="lg" mr={4} />
<Text>Loading Current Value</Text>
</Flex>
)}
{validating && (
<Stack align="center" mb={4}>
<Spinner thickness="4px" speed="1.25s" size="lg" />
<Text>Validating</Text>
</Stack>
)}
{typeof children === 'function'
? children.call(null, {
register,
control,
loading: !user,
errored: !!errors[field],
dirty: current !== existing,
current,
setter,
})
: children}
<Box>
<FormErrorMessage style={{ justifyContent: 'center' }}>
{errors[field]?.message}
</FormErrorMessage>
</Box>
</FormControl>
<Wrap>
<WrapItem>
<StatusedSubmitButton label={nextButtonLabel} {...{ status }} />
</WrapItem>
{onClose && (
<WrapItem>
<Button
variant="ghost"
onClick={onClose}
color="white"
_hover={{ bg: '#FFFFFF11' }}
_active={{ bg: '#FF000011' }}
>
Close
</Button>
</WrapItem>
)}
</Wrap>
</FlexContainer>
<WizardPane {...{ field, onSave, value, ...props }}>{children}</WizardPane>
);
};

View File

@@ -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<React.SetStateAction<number | null>>;
};
import { ProfileWizardPane } from './ProfileWizardPane';
import { WizardPaneCallbackProps } from './WizardPane';
export const SetupAvailability: React.FC<SetupAvailabilityProps> = ({
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 (
<FlexContainer mb={8}>
<MetaHeading mb={5} textAlign="center">
Avail&#xAD;ability
</MetaHeading>
<Text mb={10}>
What is your weekly availability for any kind of freelance work?
</Text>
<InputGroup borderColor="transparent" mb={10} maxW="20rem">
<InputLeftElement>
<span role="img" aria-label="clock">
🕛
</span>
</InputLeftElement>
<Input
background="dark"
placeholder="40"
type="number"
value={available ?? undefined}
onChange={({ target: { value } }) => {
setAvailability(parseFloat(value));
}}
isInvalid={invalid}
/>
<InputRightAddon background="purpleBoxDark">hr week</InputRightAddon>
</InputGroup>
<ProfileWizardPane
{...{ field }}
title="Avail&#xAD;ability"
prompt="What is your weekly availability for any kind of freelance work?"
>
{({ register, errored = false }: WizardPaneCallbackProps) => {
const { ref: registerRef, ...props } = register(field, {
valueAsNumber: true,
min: {
value: 0,
message: 'Its not possible to be available for negative time.',
},
max: {
value: 24 * 7,
message: `Theres only ${24 * 7} hours in a week.`,
},
});
<MetaButton
disabled={!user}
onClick={handleNextPress}
mt={10}
isDisabled={invalid}
isLoading={updateProfileRes.fetching || loading}
loadingText="Saving…"
>
{nextButtonLabel}
</MetaButton>
</FlexContainer>
return (
<InputGroup
mb={10}
maxW="10rem"
margin="auto"
borderColor="purple.700"
sx={{
':hover, :focus-within': {
borderColor: errored ? 'red' : 'white',
},
}}
>
<InputLeftElement>
<Text as="span" role="img" aria-label="clock">
🕛
</Text>
</InputLeftElement>
<Input
type="number"
placeholder="23…"
pl={9}
background="dark"
borderTopEndRadius={0}
borderBottomEndRadius={0}
borderRight={0}
_focus={errored ? { borderColor: 'red' } : undefined}
autoFocus
ref={(ref) => {
ref?.focus();
registerRef(ref);
}}
{...props}
/>
<InputRightAddon bg="purpleBoxDark" color="white">
<Text as="sup">hr</Text> <Text as="sub">week</Text>
</InputRightAddon>
</InputGroup>
);
}}
</ProfileWizardPane>
);
};

View File

@@ -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<number>>) => Maybe<number>),
) => void;
types: NonNullable<PersonalityInfo>;
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<ColorButtonsProps> = ({
mask,
setMask,
types,
disabled = false,
}) => (
<Wrap spacing={[3, 7]} maxW="70rem" justify="center">
{Object.entries(MaskImages)
.reverse()
.map(([bitString, image], idx) => {
const type = types[bitString];
if (!type) {
return (
<Text textAlign="center">
Could not find a type for 0b
{Number(bitString).toString(2).padStart(5, '0')}.
</Text>
);
}
const { name, mask: bit = 0, description } = type;
const selected = (mask & bit) > 0;
return (
<WrapItem key={bit}>
<Button
p={4}
w={{ base: '100%', md: 'auto' }}
borderRadius="lg"
cursor="pointer"
height="auto"
onClick={() =>
setMask((previous) =>
toggleBit({ base: previous ?? undefined, bit }),
)
}
ref={(input) => {
if (idx === 0 && !input?.getAttribute('focused-once')) {
input?.focus();
input?.setAttribute('focused-once', 'true');
}
}}
transition="background 0.25s, filter 0.75s"
bg={selected ? 'purpleBoxDark' : 'purpleBoxLight'}
_hover={{ filter: 'hue-rotate(25deg)' }}
_focus={{
borderColor: '#FFFFFF88',
outline: 'none',
filter: 'brightness(1.25)',
}}
_active={{
bg: selected ? 'purpleBoxDark' : 'purpleBoxLight',
}}
borderWidth={2}
borderColor={selected ? 'purple.400' : 'transparent'}
isDisabled={disabled}
>
<Flex>
<Image
w="100%"
maxW={16}
h={16}
mr={2}
src={image}
alt={name}
filter="drop-shadow(0px 0px 3px black)"
/>
<Stack>
<Text color="white" casing="uppercase">
{name}
</Text>
<Text
color="blueLight"
fontWeight="normal"
whiteSpace="initial"
>
{description}
</Text>
</Stack>
</Flex>
</Button>
</WrapItem>
);
})}
</Wrap>
);
export const SetupColorDisposition: React.FC<MaybeModalProps> = ({
buttonLabel,
onClose,
}) => {
const field = 'colorMask';
const [types, setTypes] = useState<Maybe<Record<number, PersonalityOption>>>(
null,
);
useEffect(() => {
const load = async () => {
setTypes(await getPersonalityInfo());
};
load();
}, []);
return (
<ProfileWizardPane
{...{ field, buttonLabel, onClose }}
title="Color Dis&#xAD;po&#xAD;sit&#xAD;ion"
prompt={
<Text textAlign="center" maxW="30rem">
Please select your personality components below. Not sure what type
you are?
<Text as="span"> Take </Text>
<MetaLink
href="//dysbulic.github.io/5-color-radar/#/explore/"
isExternal
>
a quick exam
</MetaLink>
<Text as="span"> or </Text>
<MetaLink
href="//dysbulic.github.io/5-color-radar/#/test/"
isExternal
>
a longer quiz
</MetaLink>
.
</Text>
}
>
{({
register,
loading,
current = 0,
setter,
}: WizardPaneCallbackProps<number>) => {
if (types == null) {
return (
<Text fontStyle="italic" textAlign="center">
Loading Personality Information
</Text>
);
}
return (
<Stack align="center" mt={10}>
<Input type="hidden" {...register(field, {})} />
<ColorButtons
mask={current}
setMask={setter}
disabled={loading}
{...{ types }}
/>
{!loading && (
<ColorBar
{...{ types, loading }}
mask={current ?? null}
mt={5}
w="min(90vw, 30rem)"
/>
)}
</Stack>
);
}}
</ProfileWizardPane>
);
};

View File

@@ -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 (
<FlexContainer flex={1} mb={8}>
<MetaHeading mb={10}>Game On!</MetaHeading>
@@ -20,18 +17,31 @@ export const SetupDone: React.FC = () => {
align="center"
>
{user && <PlayerTile player={user} />}
<MetaButton
onClick={() => {
setLoading(true);
router.push('/');
}}
px={20}
py={8}
fontSize="xl"
isLoading={loading}
>
Play
</MetaButton>
<Stack>
<MetaButton
as="a"
href="//discord.gg/metagame"
target="_blank"
px={20}
py={8}
fontSize="xl"
>
Play
</MetaButton>
<MetaButton
as="a"
href="/dashboard"
px={20}
py={8}
mt={{
base: '0.5rem !important',
md: '5rem !important',
}}
fontSize="xl"
>
Explore
</MetaButton>
</Stack>
</Stack>
</FlexContainer>
);

View File

@@ -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 (
<Grid templateColumns={templateColumns.join(' ')} gap="1rem" w="100%">
<Grid templateColumns={templateColumns.join(' ')} gap={[1, 4]} w="full">
<FlexContainer justify="flex-end" onClick={onBackPress} cursor="pointer">
<BoxedNextImage src={BackImage} height={5} width={5} alt="Back" />
<Text fontSize={25} fontFamily="heading">
&lt;
</Text>
</FlexContainer>
{options.sections.map((option, id) => (
{sections.map(({ label, title }, id) => (
<SectionProgress
key={option.label}
title={option.title}
key={label}
{...{ title }}
isActive={sectionIndex === id}
isDone={sectionIndex > id}
/>
))}
<FlexContainer justify="flex-end" onClick={onNextPress} cursor="pointer">
<BoxedNextImage src={SkipImage} height={5} width={5} alt="Forward" />
<Text fontSize={25} fontFamily="heading">
&gt;
</Text>
</FlexContainer>
</Grid>
);
};
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<StepProps> = ({
isActive,
}) => {
const { options, stepIndex } = useSetupFlow();
const progress = isDone ? 100 : options.progressWithinSection(stepIndex);
return (
<FlexContainer pos="relative">
<ResponsiveText
@@ -60,16 +72,14 @@ export const SectionProgress: React.FC<StepProps> = ({
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}
/>
<Flex
bgColor="blue20"
w="100%"
h="0.5rem"
borderRadius="0.25rem"
overflow="hidden"
>
<Flex bgColor="blue20" w="full" h={2} borderRadius="sm" overflow="hidden">
{(isActive || isDone) && (
<Box bgColor="purple.400" w={`${progress}%`} />
)}
@@ -78,12 +88,12 @@ export const SectionProgress: React.FC<StepProps> = ({
<BoxedNextImage
pos="absolute"
mt={24}
w="1.5rem"
h="1.75rem"
w={[4, 6]}
h={[5, 7]}
src={LogoImage}
left={`${progress}%`}
transform="translateX(-50%)"
alt="Avatar"
alt="˅"
/>
)}
</FlexContainer>

View File

@@ -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<Membership> | null | undefined;
setMemberships: React.Dispatch<
React.SetStateAction<Array<Membership> | null | undefined>
React.SetStateAction<Optional<Maybe<Array<Membership>>>>
>;
};
export const SetupMemberships: React.FC<SetupMembershipsProps> = ({
memberships,
}) => {
const { connected } = useWeb3();
const { connecting, connected } = useWeb3();
const { onNextPress, nextButtonLabel } = useSetupFlow();
const [loading, setLoading] = useState(false);
const mounted = useMounted();
return (
<FlexContainer mb={8}>
<MetaHeading mb={5} textAlign="center">
Memberships
Member&shy;ships
</MetaHeading>
{!memberships && (
<Text mb={10} maxW="50rem">
{connected ? 'Loading…' : 'Account Not Connected'}
</Text>
)}
{memberships &&
(memberships.length > 0 ? (
<Box maxW="50rem">
<Text mb={10}>
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 (
<Flex>
<Spinner mr={4} />
<Text mb={10} maxW="50rem">
{!mounted || connecting || connected
? 'Loading…'
: 'Account Not Connected'}
</Text>
</Flex>
);
}
if (memberships.length === 0) {
return (
<Text mb={10} maxW="50rem">
We did not find any guilds associated with your account.
</Text>
<SimpleGrid columns={2} spacing={4}>
{memberships.map((member) => (
<MembershipListing key={member.id} member={member} />
);
}
return (
<Box maxW="50rem">
<Text mb={10} maxW="35rem" textAlign="center">
We found the following guilds associated with your account and
automatically added them to your profile.
</Text>
<Wrap columns={2} spacing={4} justify="center">
{memberships?.map((member) => (
<WrapItem key={member.id}>
<MembershipListing {...{ member }} />
</WrapItem>
))}
</SimpleGrid>
</Wrap>
</Box>
) : (
<Text mb={10} maxW="50rem">
We did not find any guilds associated with your account.
</Text>
))}
);
})()}
<MetaButton
onClick={() => {
setLoading(true);
@@ -81,49 +98,52 @@ type MembershipListingProps = {
member: Membership;
};
const MembershipListing: React.FC<MembershipListingProps> = ({ member }) => {
const daoUrl = getDaoLink(member.moloch.chain, member.moloch.id);
const { avatarUrl, chain, title } = member.moloch;
const MembershipListing: React.FC<MembershipListingProps> = ({
member: { moloch },
}) => {
const { id: molochId, avatarURL, chain, title } = moloch;
const daoURL = getDAOLink(chain, molochId);
return (
<DaoHausLink daoUrl={daoUrl}>
<HStack alignItems="center" mb={4}>
<Flex bg="purpleBoxLight" width={16} height={16} mr={6}>
{avatarUrl ? (
<DaoHausLink
{...{ daoURL }}
bg="dark"
border="2px transparent solid"
_hover={{ borderColor: 'purpleBoxLight' }}
>
<HStack align="center">
<Flex bg="purpleBoxLight" width={16} height={16} mr={1}>
{avatarURL ? (
<Image
src={avatarUrl}
src={avatarURL}
w="3.25rem"
h="3.25rem"
m="auto"
borderRadius={4}
/>
) : (
<ChainIcon chain={chain} boxSize={16} p={2} />
<ChainIcon {...{ chain }} boxSize={16} p={2} />
)}
</Flex>
<Box>
<Heading
fontWeight="bold"
textTransform="uppercase"
fontSize="xs"
color={daoUrl ? 'cyanText' : 'white'}
mb={1}
>
<Center justifyContent="left">
{title ?? (
<Text>
Unknown{' '}
<Text as="span" textTransform="capitalize">
{chain}
</Text>{' '}
DAO
</Text>
)}
<ChainIcon chain={chain} ml={2} boxSize={3} />
</Center>
</Heading>
</Box>
<Heading
fontWeight="bold"
textTransform="uppercase"
fontSize="xs"
color={daoURL ? 'cyanText' : 'white'}
justify="center"
align="center"
>
{title ?? (
<Text as={React.Fragment}>
Unknown{' '}
<Text as="span" textTransform="capitalize">
{chain}
</Text>{' '}
DAO
</Text>
)}
<ChainIcon {...{ chain }} mx={2} boxSize={4} />
</Heading>
</HStack>
</DaoHausLink>
);

View File

@@ -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<SetupPersonalityTypeProps> = ({
isEdit,
onClose,
}) => {
const { onNextPress, nextButtonLabel } = useSetupFlow();
const { fetching: fetchingUser, user } = useUser();
const { ceramic } = useWeb3();
const toast = useToast();
const [status, setStatus] = useState<Maybe<ReactElement | string>>(null);
const [colorMask, setColorMask] = useState<Maybe<number> | undefined>();
const [types, setPersonalityInfo] = useState<PersonalityInfo>({});
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(<Text>Authenticating DID</Text>);
await ceramic.did?.authenticate();
}
setStatus(<Text>Saving Color Disposition</Text>);
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 = (
<FlexContainer mb={isWizard ? 8 : 0} spacing={isWizard ? 8 : 4}>
{isWizard && (
<MetaHeading textAlign="center">Person&#xAD;ality Type</MetaHeading>
)}
<Text color={isWizard ? 'current' : 'white'}>
Please select your personality components below. Not sure what type you
are?
<Text as="span"> Take </Text>
<MetaLink
href="//dysbulic.github.io/5-color-radar/#/explore/"
isExternal
>
a quick exam
</MetaLink>
<Text as="span"> or </Text>
<MetaLink href="//dysbulic.github.io/5-color-radar/#/test/" isExternal>
a longer quiz
</MetaLink>
.
</Text>
{fetching ? (
<LoadingState />
) : (
<>
<Wrap spacing={2} justify="center" maxW="70rem">
{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 (
<WrapItem>
<Button
key={mask}
display="flex"
direction="row"
justifyContent="start"
p={6}
m={2}
h="auto"
w={{ base: '100%', md: 'auto' }}
spacing={4}
borderRadius={8}
cursor="pointer"
onClick={() => toggleMaskElement(mask)}
autoFocus={idx === 0} // Doesn't work
ref={(input) => {
if (
idx === 0 &&
!input?.getAttribute('focused-once')
) {
input?.focus();
input?.setAttribute('focused-once', 'true');
}
}}
onKeyPress={(e) => {
if (e.key === 'Enter') {
if (isWizard) handleNextPress();
if (isEdit) save();
e.preventDefault();
}
}}
transition="background 0.25s, filter 0.5s"
bgColor={selected ? 'purpleBoxDark' : 'purpleBoxLight'}
_hover={{
filter: 'hue-rotate(25deg)',
}}
_focus={{
borderColor: '#FFFFFF55',
outline: 'none',
}}
_active={{
bg: selected ? 'purpleBoxDark' : 'purpleBoxLight',
}}
borderWidth={2}
borderColor={selected ? 'purple.400' : 'transparent'}
>
<Image
w="100%"
maxW={16}
h={16}
mr={2}
src={image}
alt={option?.name}
filter="drop-shadow(0px 0px 3px black)"
/>
<FlexContainer align="stretch" ml={2}>
<Text
color="white"
casing="uppercase"
textAlign="left"
>
{option?.name}
</Text>
<Text
color="blueLight"
fontWeight="normal"
whiteSpace="initial"
textAlign="left"
>
{option?.description}
</Text>
</FlexContainer>
</Button>
</WrapItem>
);
})}
</Wrap>
<ColorBar
mask={colorMask ?? null}
mt={8}
w="min(90vw, 30rem)"
{...{ types }}
/>
{isWizard && (
<MetaButton
onClick={handleNextPress}
mt={10}
isDisabled={!colorMask}
isLoading={!!status}
loadingText={status?.toString() ?? undefined}
>
{nextButtonLabel}
</MetaButton>
)}
</>
)}
</FlexContainer>
);
return isWizard ? (
setup
) : (
<>
<ModalBody>{setup}</ModalBody>\
{isEdit && onClose && (
<FlexContainer>
<ModalFooter py={6}>
<Wrap justify="center" align="center" flex={1}>
<WrapItem>
<MetaButton
isDisabled={!!status}
onClick={async () => {
await save();
onClose();
}}
>
{!status ? (
'Save Changes'
) : (
<Flex align="center">
<Spinner mr={3} />
{typeof status === 'string' ? (
<Text>{status}</Text>
) : (
status
)}
</Flex>
)}
</MetaButton>
</WrapItem>
<WrapItem>
<Button
variant="ghost"
onClick={onClose}
color="white"
_hover={{ bg: '#FFFFFF11' }}
_active={{ bg: '#FF000011' }}
>
Close
</Button>
</WrapItem>
</Wrap>
</ModalFooter>
</FlexContainer>
)}
</>
);
};

View File

@@ -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<string>;
setSelectedType: (
arg: string | ((type: Optional<Maybe<string>>) => Maybe<string>),
) => void;
disabled?: boolean;
};
export const SetupPlayerType: React.FC<Props> = ({ isEdit, onClose }) => {
const { onNextPress, nextButtonLabel } = useSetupFlow();
const { user } = useUser();
const { ceramic } = useWeb3();
const toast = useToast();
const [status, setStatus] = useState<Maybe<ReactElement | string>>(null);
const [explorerType, setExplorerType] = useState<ExplorerType>();
const [typeChoices, setTypeChoices] = useState<ExplorerType[]>([]);
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<ExplorerTypesType> = ({
selectedType,
setSelectedType,
disabled = false,
}) => {
const [choices, setChoices] = useState<Array<ExplorerType>>([]);
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 (
<InputGroup>
<SimpleGrid
maxW={{ base: '25rem', md: '60rem' }}
columns={{ base: 1, md: 3 }}
spacing={4}
margin="auto"
>
{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 = (
<FlexContainer mb={8}>
{isWizard && (
<MetaHeading mb={5} textAlign="center">
Player Type
</MetaHeading>
)}
<Text mb={10} color={isWizard ? 'current' : 'white'}>
Please read the features of each player type below, and select the one
that suits you best.
</Text>
<SimpleGrid columns={[1, null, 3, 3]} spacing={4}>
{typeChoices.map((choice) => (
<FlexContainer
key={choice.id}
p={[4, null, 6]}
bgColor={
explorerType?.id === choice.id
? 'purpleBoxDark'
: 'purpleBoxLight'
}
borderRadius="0.5rem"
_hover={{ bgColor: 'purpleBoxDark' }}
transition="background 0.25s"
cursor="pointer"
onClick={() => setExplorerType(choice)}
align="stretch"
justify="flex-start"
border="2px"
borderColor={
explorerType?.id === choice.id ? 'purple.400' : 'transparent'
}
>
<Text color="white" fontWeight="bold" mb={4}>
{choice.title}
</Text>
<Text color="blueLight" textAlign="justify">
{choice.description}
</Text>
</FlexContainer>
))}
return (
<Button
height="full"
whiteSpace="normal"
key={choice.id}
p={[4, null, 6]}
bgColor={selected ? 'purpleBoxDark' : 'purpleBoxLight'}
borderRadius="lg"
_hover={{ filter: 'hue-rotate(-10deg)' }}
cursor="pointer"
onClick={() => setSelectedType(choice.title)}
align="stretch"
justify="flex-start"
border="2px"
borderColor={selected ? 'purple.400' : 'transparent'}
isDisabled={disabled}
>
<Stack>
<Text color="white" fontWeight="bold" mb={4}>
{choice.title}
</Text>
<Text color="blueLight" textAlign="justify" fontWeight="normal">
{choice.description}
</Text>
</Stack>
</Button>
);
})}
</SimpleGrid>
{isWizard && (
<MetaButton
onClick={handleNextPress}
mt={10}
isDisabled={!explorerType}
isLoading={!!status}
loadingText={status?.toString()}
>
{nextButtonLabel}
</MetaButton>
)}
</FlexContainer>
);
return isWizard ? (
setup
) : (
<>
<ModalBody>{setup}</ModalBody>
{isEdit && onClose && (
<FlexContainer>
<ModalFooter py={6}>
<Wrap justify="center" align="center" flex={1}>
<WrapItem>
<MetaButton
isDisabled={!!status}
onClick={async () => {
await save();
onClose();
}}
>
{!status ? (
'Save Changes'
) : (
<Flex align="center">
<Spinner mr={3} />
{typeof status === 'string' ? (
<Text>{status}</Text>
) : (
status
)}
</Flex>
)}
</MetaButton>
</WrapItem>
<WrapItem>
<Button
variant="ghost"
onClick={onClose}
color="white"
_hover={{ bg: '#FFFFFF11' }}
_active={{ bg: '#FF000011' }}
disabled={!!status}
>
Close
</Button>
</WrapItem>
</Wrap>
</ModalFooter>
</FlexContainer>
)}
</>
</InputGroup>
);
};
export const SetupPlayerType: React.FC<MaybeModalProps> = ({
onClose,
buttonLabel,
}) => {
const field = 'explorerTypeTitle';
return (
<ProfileWizardPane
{...{ field, onClose, buttonLabel }}
title="Player Type"
prompt="Which one suits you best?"
>
{({ register, loading, current, setter }: WizardPaneCallbackProps) => (
<Center mt={5}>
<Input type="hidden" {...register(field, {})} />
<ExplorerTypes
selectedType={current}
setSelectedType={setter}
disabled={loading}
/>
</Center>
)}
</ProfileWizardPane>
);
};

View File

@@ -9,7 +9,7 @@ export const SetupProfile: React.FC = ({ children }) => {
return (
<PageContainer>
{options.numSteps - 1 > stepIndex && <SetupHeader />}
<FlexContainer flex={1} pt={{ base: 16, sm: 24 }}>
<FlexContainer flex={1} pt={[10, 12]}>
{children}
</FlexContainer>
</PageContainer>

View File

@@ -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<React.SetStateAction<string | undefined>>;
};
import { ProfileWizardPane } from './ProfileWizardPane';
import { WizardPaneCallbackProps } from './WizardPane';
export const SetupPronouns: React.FC<SetupPronounsProps> = ({
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 (
<FlexContainer mb={8}>
<MetaHeading mb={10} textAlign="center">
Which pronouns do you prefer?
</MetaHeading>
<Input
background="dark"
placeholder="they/them"
value={pronouns ?? ''}
onChange={({ target: { value } }) => setPronouns(value)}
w="auto"
/>
<ProfileWizardPane
{...{ field }}
title="Pronouns"
prompt="Which pronouns do you prefer?"
>
{({ register, errored }: WizardPaneCallbackProps) => {
const { ref: registerRef, ...props } = register(field, {
maxLength: {
value: 150,
message: 'Maximum length is 150 characters.',
},
});
<MetaButton
onClick={handleNextPress}
disabled={!user}
mt={10}
isLoading={fetching || loading}
loadingText="Saving…"
>
{nextButtonLabel}
</MetaButton>
</FlexContainer>
return (
<Flex justify="center" mt={5}>
<Input
background="dark"
placeholder="they / them"
w="auto"
_focus={errored ? { borderColor: 'red' } : undefined}
ref={(ref) => {
ref?.focus();
registerRef(ref);
}}
{...props}
/>
</Flex>
);
}}
</ProfileWizardPane>
);
};

View File

@@ -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<PlayerRole>;
choices?: Maybe<Array<PlayerRole>>;
isEdit?: boolean;
onClose?: () => void;
buttonLabel?: Optional<string | ReactElement>;
};
export const SetupRoles: React.FC<SetupRolesProps> = ({
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<PlayerRole[]>(
inputRoleChoices,
const field = 'roles';
const { user } = useUser();
const [choices, setChoices] = useState<Maybe<Array<PlayerRole>>>(
inputChoices,
);
const [, updateRoles] = useUpdateRoles();
const { value: roles, setter: setRoles } = useOverridableField<Array<string>>(
{
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<string[]>([]);
}, [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<string, unknown>;
setStatus?: (msg: string) => void;
}) => {
const { roles: toSet } = values as { ['roles']: Array<string> };
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 = (
<FlexContainer
align="center"
mx={isWizard ? { base: 0, md: 8, lg: 16 } : 0}
color="white"
mb={isWizard ? 'auto' : 0}
return (
<WizardPane<Array<string>>
{...{ field, onClose, onSave, buttonLabel }}
value={roles}
title="Roles"
prompt={
<Text mb={[4, 6]} textAlign="center">
Unlike other role-playing games, in MetaGame a player is free to take
multiple roles at the same time.
</Text>
}
fetching={!user}
>
{isWizard && (
<MetaHeading mb={{ base: 6, sm: 16 }} alignSelf="center">
Select your role(s)
</MetaHeading>
)}
{fetching && <LoadingState />}
{!fetching &&
(roles.length === 0 ? (
<Text mb={{ base: 6, sm: 10 }}>
Unlike other role-playing games, in MetaGame, anyone is free to play
multiple roles at the same time.
<br />
Players are required to specify their primary role, whereas any
secondary roles are optional.
</Text>
) : (
<Flex wrap="wrap" mb={{ base: 4, md: 16 }} w="100%">
{roles.map((r, i) => {
const choice = roleChoices.find(
(roleChoice) => roleChoice.role === r,
);
return (
choice && (
<>
<Box key={r} {...roleContainerStyles}>
<Text
color="cyan.500"
fontWeight="bold"
casing="uppercase"
my="2"
>
{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 && (
<span>&nbsp;</span>
)}
</Text>
{({
register,
current,
setter,
}: WizardPaneCallbackProps<Array<string>>) => {
if (!choices) {
return <Text>Loading Role Choices</Text>;
}
<Role
role={choice}
selectionIndex={i}
numSelectedRoles={roles.length}
onSelect={handleSelection}
onRemove={handleRemoval}
/>
</Box>
{/* wrap after the primary */}
{i === 0 && <Box flexBasis="100%" />}
</>
)
);
})}
</Flex>
))}
{availableRoles.length > 0 && !fetching && (
<>
<Text
alignSelf="flex-start"
color="white"
fontWeight="bold"
casing="uppercase"
my="2"
>
Available Roles
</Text>
<Flex wrap="wrap" mb={{ base: 6, md: 16 }}>
{availableRoles.map((r) => (
<Box key={r.role} {...roleContainerStyles}>
<Role role={r} onSelect={handleSelection} />
</Box>
))}
</Flex>
</>
)}
if (!current) return null;
{isWizard && !fetching && (
<FlexContainer pb={8}>
<MetaButton
onClick={handleNextPress}
isDisabled={roles.length < 1}
isLoading={updateRolesResult.fetching || loading}
loadingText="Saving"
>
{nextButtonLabel}
</MetaButton>
</FlexContainer>
)}
</FlexContainer>
);
return isWizard ? (
setup
) : (
<>
<ModalBody>{setup} </ModalBody>
{isEdit && onClose && (
<FlexContainer>
<ModalFooter py={8}>
<MetaButton
mr={3}
isLoading={loading}
loadingText="Saving…"
onClick={async () => {
await save();
onClose();
}}
>
Save Changes
</MetaButton>
<Button
variant="ghost"
onClick={onClose}
color="white"
_hover={{ bg: '#FFFFFF11' }}
_active={{ bg: '#FF000011' }}
disabled={loading}
>
Close
</Button>
</ModalFooter>
</FlexContainer>
)}
</>
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 (
<Stack mb={[4, 8]}>
<Input type="hidden" {...register(field, {})} />
<RoleGroup
title="Primary Role"
active={true}
primary={true}
roles={current.slice(0, 1)}
numSelectedRoles={current.length}
{...{ mobile, choices, select, remove }}
/>
<RoleGroup
title="Secondary Role"
active={true}
roles={current.slice(1)}
numSelectedRoles={current.length}
{...{ mobile, choices, select, remove }}
/>
<RoleGroup
title="Available Role"
roles={availableRoles}
{...{ mobile, choices, select }}
/>
</Stack>
);
}}
</WizardPane>
);
};
export type RoleGroupProps = {
roles: Array<string>;
choices: Array<PlayerRole>;
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<RoleGroupProps> = ({
roles,
choices,
title,
active,
primary,
numSelectedRoles,
select,
remove,
mobile,
}) =>
roles.length === 0 ? null : (
<Box mr={[0, 4]} my={[2, 4]}>
{title && (
<Heading
flexDirection="column"
color={active ? 'cyan.500' : 'white'}
fontWeight="bold"
casing="uppercase"
my={2}
fontSize={['xs', 'sm']}
>
{title}
{roles.length > 1 ? 's' : null}
</Heading>
)}
<SimpleGrid
gap={[1.5, 5]}
mx="auto"
maxW={['16rem', '17rem', '35rem', '35rem', '55rem', '72rem']}
columns={[1, 1, 2, 2, 3, 4]}
>
{roles.map((r) => {
const choice = choices?.find(({ role }) => role === r);
return (
<React.Fragment key={r}>
{!choice ? (
<Text textStyle="error">Couldn't find role “{r}”.</Text>
) : (
<Role
role={choice}
selected={active}
onSelect={select}
onRemove={remove}
{...{ primary, numSelectedRoles, mobile }}
/>
)}
</React.Fragment>
);
})}
</SimpleGrid>
</Box>
);
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<RoleProps> = ({
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 (
<Box
p={{ base: 2, lg: 6 }}
py={{ base: selected ? 1.5 : 2, lg: 6 }}
bgColor="purpleBoxLight"
borderRadius="0.5rem"
_hover={selectionIndex == null ? { bgColor: 'purpleBoxDark' } : {}}
cursor={selectionIndex == null ? 'pointer' : 'default'}
_hover={!selected ? { bgColor: 'purpleBoxDark' } : undefined}
cursor={!selected ? 'pointer' : 'default'}
transition="background 0.25s"
border="2px"
borderColor="purple.400"
px={4}
onClick={handleContainerClick}
h={selectionIndex != null ? 'auto' : '100%'}
px={[selected ? 1.5 : 3, 4]}
h={selected ? 'auto' : '100%'}
w="full"
{...{ onClick }}
>
<Flex
direction={{ base: 'row', md: 'column' }}
align="center"
justify={{ base: 'space-between', md: 'stretch' }}
>
<Flex h="100%" direction={['row', 'column']} align="center">
<BoxedNextImage
src={`/assets/roles/${role.role.toLowerCase()}.svg`}
alt={role.label}
height={{ base: 4, md: 14 }}
width={{ base: 4, md: 14 }}
h={[6, 14]}
minW={[selected ? 4 : 6, 14]}
mr={2}
/>
<Text
color="white"
fontWeight="bold"
casing="uppercase"
my={{ base: 0, md: 2 }}
my={[0, 2]}
letterSpacing="tight"
onClick={(evt) => {
if (selected) {
evt.stopPropagation();
setShowDetails((show) => !show);
}
}}
>
{role.label}
</Text>
{!isMobile && <Text color="white">{role.description}</Text>}
<Spacer />
{isMobile && (
{!mobile && (
<Text color="white" textAlign="justify">
{role.description}
</Text>
)}
<Spacer direction="column" />
{mobile && (numSelectedRoles == null || numSelectedRoles <= 1) && (
<InfoIcon
ml={2}
ml={1}
cursor="pointer"
transform={showDetails ? 'rotate(-180deg)' : undefined}
transition="0.5s"
onClick={(e) => {
e.stopPropagation();
setShowDetails(!showDetails);
setShowDetails((show) => !show);
}}
/>
)}
{selectionIndex != null && (
<Flex
w="100%"
justifyContent={{ base: 'end', md: 'space-between' }}
mt={{ base: 0, md: 4 }}
ml={2}
>
{selected && (
<Flex justifyContent={['end', 'space-between']} mt={[0, 4]} ml={2}>
{numSelectedRoles != null &&
numSelectedRoles > 1 &&
(isMobile ? (
(mobile ? (
<Button
variant="solid"
textTransform="uppercase"
@@ -365,10 +337,11 @@ const Role: React.FC<RoleProps> = ({
borderColor="purple.200"
size="xs"
whiteSpace="pre-wrap"
mr={2}
onClick={() => onSelect(role, selectionIndex !== 0)}
mr={1}
px={1}
onClick={() => onSelect?.(role, !primary)}
>
Make {selectionIndex === 0 ? 'Secondary' : 'Primary'}
Make {primary ? 'Secondary' : 'Primary'}
</Button>
) : (
<Button
@@ -378,18 +351,19 @@ const Role: React.FC<RoleProps> = ({
color="purple.200"
borderColor="purple.200"
_hover={{
borderColor: 'transparent',
borderColor: 'purple.900',
bgColor: 'blackAlpha.300',
}}
fontSize={{ md: '0.875rem', lg: '1rem' }}
fontSize="sm"
borderWidth={2}
mr={3}
whiteSpace="pre-wrap"
onClick={() => onSelect(role, selectionIndex !== 0)}
onClick={() => onSelect?.(role, !primary)}
>
Make {selectionIndex === 0 ? 'Secondary' : 'Primary'}
Make {primary ? 'Secondary' : 'Primary'}
</Button>
))}
{isMobile ? (
{mobile ? (
<Button
variant="solid"
fontWeight="bold"
@@ -397,8 +371,7 @@ const Role: React.FC<RoleProps> = ({
color="white"
bgColor="red.500"
size="xs"
whiteSpace="pre-wrap"
onClick={handleRemoveClick}
onClick={() => onRemove?.(role)}
>
<CloseIcon />
</Button>
@@ -411,9 +384,8 @@ const Role: React.FC<RoleProps> = ({
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
</Button>
@@ -422,7 +394,7 @@ const Role: React.FC<RoleProps> = ({
)}
</Flex>
{showDetails && (
<Text color="white" mt={4}>
<Text color="white" mt={4} textAlign="justify">
{role.description}
</Text>
)}

View File

@@ -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<SetupSkillsProps> = ({
isEdit,
export const SetupSkills: React.FC<MaybeModalProps> = ({
onClose,
buttonLabel,
}) => {
const { onNextPress, nextButtonLabel } = useSetupFlow();
const { user } = useUser({ requestPolicy: 'network-only' });
const toast = useToast();
const [skillChoices, setSkillChoices] = useState<Array<CategoryOption>>([]);
const [updateSkillsRes, updateSkills] = useUpdatePlayerSkillsMutation();
const [loading, setLoading] = useState(false);
const [playerSkills, setPlayerSkills] = useState<Array<SkillOption>>([]);
const isWizard = !isEdit;
const field = 'skills';
const mounted = useMounted();
const [choices, setChoices] = useState<Array<CategoryOption>>();
const { user } = useUser();
const { value: strippedSkills, setter: setValue } = useOverridableField<
Array<SkillOption>
>({
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<string, unknown>;
setStatus?: (msg: string) => void;
}) => {
setStatus?.('Writing to Hasura…');
const { error } = await updateSkills({
skills: playerSkills.map(({ id }) => ({ skill_id: id })),
skills: (skillList as Array<SkillOption>).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 = (
<FlexContainer mb={8}>
{isWizard && (
<MetaHeading mb={5} textAlign="center">
What are your super&#xAD;powers?
</MetaHeading>
)}
<FlexContainer w="100%" align="stretch" maxW="50rem">
<SelectSearch
isMulti
styles={styles}
value={playerSkills}
onChange={(value) => setPlayerSkills(value as Array<SkillOption>)}
options={skillChoices}
autoFocus
closeMenuOnSelect={false}
placeholder="Add Your Skills…"
/>
</FlexContainer>
return (
<WizardPane<Array<SkillOption>>
{...{ field, onClose, onSave, buttonLabel }}
title="Skills"
prompt="What are your super&#xAD;powers?"
fetching={!user}
value={skills}
>
{({
register,
setter,
current,
}: WizardPaneCallbackProps<Array<SkillOption>>) => {
const { ref: registerRef, onChange, ...props } = register(field, {});
{isWizard && (
<MetaButton
onClick={handleNextPress}
mt={10}
isLoading={updateSkillsRes.fetching || loading}
loadingText="Saving…"
>
{nextButtonLabel}
</MetaButton>
)}
</FlexContainer>
);
return isWizard ? (
setup
) : (
<>
<ModalBody> {setup} </ModalBody>
{isEdit && onClose && (
<FlexContainer>
<ModalFooter py={6}>
<MetaButton
mr={3}
isLoading={loading}
loadingText="Saving…"
onClick={async () => {
await save();
onClose();
if (choices == null || !mounted) {
return (
<Flex>
<Spinner />
<Text>Loading Options</Text>
</Flex>
);
}
return (
<Center w="full" align="stretch">
<SelectSearch
isMulti
{...{ styles }}
onChange={(newValue) => {
const values = (newValue as unknown) as Array<SkillOption>;
setter(values);
}}
>
Save Changes
</MetaButton>
<Button
variant="ghost"
onClick={onClose}
color="white"
_hover={{ bg: '#FFFFFF11' }}
_active={{ bg: '#FF000011' }}
disabled={loading}
>
Close
</Button>
</ModalFooter>
</FlexContainer>
)}
</>
options={choices}
value={current}
autoFocus
closeMenuOnSelect={false}
placeholder="Add your skills…"
menuShouldScrollIntoView={true}
menuPlacement={modal ? 'auto' : 'top'}
{...props}
/>
</Center>
);
}}
</WizardPane>
);
};

View File

@@ -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<string>('');
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 (
<FlexContainer mb={8}>
<MetaHeading mb={10} mt={-64} textAlign="center">
Which time zone are you in?
</MetaHeading>
<FlexContainer w="100%" align="stretch" maxW="30rem">
<SelectTimeZone
value={timeZone ?? ''}
onChange={(tz) => setTimeZone(tz.value)}
labelStyle="abbrev"
/>
</FlexContainer>
<MetaButton
disabled={!user}
onClick={handleNextPress}
mt={10}
isLoading={updateProfileRes.fetching || loading}
loadingText="Saving…"
>
{nextButtonLabel}
</MetaButton>
</FlexContainer>
<ProfileWizardPane
{...{ field }}
title="Time Zone"
prompt="Which zone are you in?"
>
{({ control }: WizardPaneCallbackProps) => (
<Center maxW="20rem" m="auto">
<Controller
{...{ control }}
name={field}
defaultValue={Intl.DateTimeFormat().resolvedOptions().timeZone}
render={({ field: { onChange, ref, ...props } }) =>
!mounted ? (
<Text>Not Mounted</Text> // avoiding “different className” error
) : (
<SelectTimeZone
labelStyle="abbrev"
onChange={(tz) => onChange(tz.value)}
{...props}
/>
)
}
/>
</Center>
)}
</ProfileWizardPane>
);
};

View File

@@ -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<React.SetStateAction<string | undefined>>;
};
import { ProfileWizardPane } from './ProfileWizardPane';
import { WizardPaneCallbackProps } from './WizardPane';
export const SetupUsername: React.FC<SetupUsernameProps> = ({
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 (
<FlexContainer mb={8}>
<MetaHeading mb={10} textAlign="center">
What username would you like?
</MetaHeading>
<Input
background="dark"
placeholder="USERNAME"
value={username ?? ''}
onChange={({ target: { value } }) => setUsername(value)}
w="auto"
/>
<ProfileWizardPane
{...{ field }}
title="Username"
prompt="What name would you like to use in your MyMeta profile URL?"
>
{({ 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.',
},
});
<MetaButton
disabled={!user}
onClick={handleNextPress}
mt={10}
isLoading={updateUsernameRes.fetching || loading}
loadingText="Saving"
>
{nextButtonLabel}
</MetaButton>
</FlexContainer>
return (
<Flex justify="center" mt={5}>
<Input
background="dark"
placeholder="sᴇʀɴᴀᴍᴇ"
w="auto"
_focus={errored ? { borderColor: 'red' } : undefined}
ref={(ref) => {
ref?.focus();
registerRef(ref);
}}
{...props}
/>
</Flex>
);
}}
</ProfileWizardPane>
);
};

View File

@@ -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<T = string> = WizardPaneProps & {
value: Optional<Maybe<T>>;
fetching?: boolean;
authenticating?: boolean;
onSave?: ({
values,
setStatus,
}: {
values: Record<string, unknown>;
setStatus?: (msg: string) => void;
}) => Promise<void>;
};
export type WizardPaneCallbackProps<T = string> = {
register: (
field: string,
opts: Record<string, unknown>,
) => UseFormRegisterReturn;
control: Control;
loading: boolean;
errored: boolean;
dirty: boolean;
current: T;
setter: (arg: T | ((prev: Optional<Maybe<T>>) => Maybe<T>)) => void;
};
export const WizardPane = <T,>({
field,
title,
prompt,
buttonLabel,
onClose,
onSave,
value: existing,
fetching = false,
children,
}: PropsWithChildren<PaneProps<T>>) => {
const { onNextPress, nextButtonLabel } = useSetupFlow();
const [status, setStatus] = useState<Maybe<string | ReactElement>>();
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 (
<Stack mt={['-12rem', '-8rem']}>
<Image w="min(40rem, 100%)" maxW="130%" src={cursiveTitle} />
<Box align="center" mt="10vh ! important">
<MetaButton onClick={connect} px={[8, 12]}>
Connect To Progress
</MetaButton>
</Box>
<Flex justify="center" mt="2rem ! important">
<Tooltip label="Join Our Discord" hasArrow>
<MetaButton
as="a"
target="_blank"
href="//discord.gg/metagame"
p={3}
mr={5}
sx={{ filter: 'saturate(60%) hue-rotate(45deg)' }}
>
<Image src={discord} boxSize={6} mr={1.5} /> Get Help
</MetaButton>
</Tooltip>
<Tooltip label="Read Our Wiki" hasArrow>
<MetaButton
as="a"
target="_blank"
href="//wiki.metagame.wtf"
p={3}
sx={{ filter: 'saturate(60%) hue-rotate(45deg)' }}
>
<chakra.span fontSize="150%">📚</chakra.span> Learn More
</MetaButton>
</Tooltip>
</Flex>
</Stack>
);
}
return (
<FlexContainer as="form" onSubmit={handleSubmit(onSubmit)} color="white">
<HeadComponent title={`MetaGame: Setting ${title}`} />
{title && (
<MetaHeading mt={8} mb={1} textAlign="center">
{title}
</MetaHeading>
)}
{prompt && (
<Box maxW="25rem">
{typeof prompt === 'string' ? (
<Text mb={0} textAlign="center">
{prompt}
</Text>
) : (
prompt
)}
</Box>
)}
<FormControl
isInvalid={!!errors[field]}
isDisabled={!connected || fetching}
>
{(!connected || fetching || validating) && (
<Flex justify="center" align="center" my={8}>
<Spinner thickness="4px" speed="1.25s" size="lg" mr={4} />
<Text>
{(() => {
if (!connected) return 'Authenticating…';
if (validating) return 'Validating…';
return 'Loading Current Value…';
})()}
</Text>
</Flex>
)}
<Box my={5}>
{typeof children === 'function'
? children.call(null, {
register,
control,
loading: !connected || fetching,
errored: !!errors[field],
dirty,
current,
setter,
})
: children}
<FormErrorMessage style={{ justifyContent: 'center' }}>
{errors[field]?.message}
</FormErrorMessage>
</Box>
</FormControl>
<Wrap align="center">
<WrapItem>
<StatusedSubmitButton
px={[8, 12]}
label={buttonLabel ?? nextButtonLabel}
{...{ status }}
/>
</WrapItem>
{onClose && (
<WrapItem>
<Button
variant="ghost"
onClick={onClose}
color="white"
_hover={{ bg: '#FFFFFF11' }}
_active={{ bg: '#FF000011' }}
>
Close
</Button>
</WrapItem>
)}
</Wrap>
</FlexContainer>
);
};

View File

@@ -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
}
}

View File

@@ -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<GuildMembership> = [
...(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,
})),
];

View File

@@ -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<string[]> => {
export const getPlayerUsernames = async (
limit = 150,
): Promise<Array<{ address: string; username: Maybe<string> }>> => {
const { data, error } = await defaultClient
.query<GetPlayerUsernamesQuery, GetPlayerUsernamesQueryVariables>(
playerUsernamesQuery,
@@ -148,15 +151,12 @@ export const getPlayerUsernames = async (limit = 150): Promise<string[]> => {
)
.toPromise();
if (!data) {
if (error) {
throw error;
}
return [];
}
return data.player
.map(({ profile }) => profile?.username ?? null)
.filter((u) => !!u) as Array<string>;
if (error) throw error;
return (data?.player ?? []).map(({ ethereumAddress: address, profile }) => ({
address,
username: profile?.username ?? null,
}));
};
export const getTopPlayerUsernames = getPlayerUsernames;

View File

@@ -1,7 +1,5 @@
export const InsertCacheInvalidation = /* GraphQL */ `
mutation InsertCacheInvalidation($playerId: uuid!) {
updateIDXProfile(playerId: $playerId) {
success
}
updateIDXProfile(playerId: $playerId)
}
`;

View File

@@ -27,7 +27,7 @@ export type PersonalityOption = {
};
export type Membership = Pick<Member, 'id'> & {
moloch: Pick<Moloch, 'id' | 'title' | 'version' | 'chain' | 'avatarUrl'>;
moloch: Pick<Moloch, 'id' | 'title' | 'version' | 'chain' | 'avatarURL'>;
};
export type MeType =

View File

@@ -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;

View File

@@ -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<Collectible>;
data: Array<Collectible>;
loading: boolean;
error: Maybe<string>;
} => {
const [favorites, setFavorites] = useState<Array<Collectible>>([]);
const [data, setData] = useState<Array<Collectible>>([]);
const [loading, setLoading] = useState<boolean>(false);
const owner = player.ethereumAddress;
const [error, setError] = useState<Maybe<string>>(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<Array<Collectible>> => {
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;
};

View File

@@ -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<T> = {
[field in keyof Profile]?: Maybe<T>;
} & {
value: Maybe<T>;
setter: Maybe<(value: unknown) => void>;
owner: Maybe<boolean>;
user: Maybe<Player>;
fetching: boolean;
};
export type ProfileValueType = string | number | Array<string> | ExplorerType;
@@ -24,40 +30,53 @@ export const clearJotaiState = () => {
export const useProfileField = <T extends ProfileValueType = string>({
field,
player = null,
owner = false,
getter = null,
}: {
field: string;
player?: Maybe<Player>;
owner?: boolean;
getter?: Maybe<(player: Maybe<Player>) => Optional<Maybe<T>>>;
}): ProfileFieldType<T> => {
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<Maybe<T>>(value);
}
// eslint-disable-next-line @typescript-eslint/no-shadow
const ret = useAtom((atom ?? nullAtom) as PrimitiveAtom<Maybe<typeof value>>);
const response = useAtom(
(atom ?? nullAtom) as PrimitiveAtom<Maybe<typeof value>>,
);
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,
};
};

View File

@@ -60,7 +60,6 @@ export const useSaveCeramicProfile = ({
useProfileField({
field: key,
player: user,
owner: true,
});
return [key, setter];
}),

View File

@@ -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<Player>;
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,
};
};

View File

@@ -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() {

View File

@@ -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"

View File

@@ -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)` });
}
}

View File

@@ -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<typeof getStaticProps>;
@@ -31,8 +31,8 @@ const GuildPage: React.FC<Props> = ({ 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<Props> = ({ guild }) => {
const getBox = (name: string): React.ReactNode => {
switch (name) {
case BoxType.GUILD_PLAYERS:
case BoxTypes.GUILD_PLAYERS:
return <GuildPlayers guildId={guild.id} guildname={guild.guildname} />;
case BoxType.GUILD_LINKS:
case BoxTypes.GUILD_LINKS:
return <GuildLinks guild={guild} />;
case BoxType.GUILD_ANNOUNCEMENTS:
case BoxTypes.GUILD_ANNOUNCEMENTS:
return (
<ProfileSection title="Announcements">
<p>No announcements.</p>

View File

@@ -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<typeof getStaticProps>;
const CurrentUserPage: React.FC<Props> = ({ personalityInfo }) => {
const CurrentUserPage: React.FC<Props> = () => {
const { connect, connecting, connected } = useWeb3();
const { user, fetching, error } = useUser();
const mounted = useMounted();
@@ -31,25 +24,33 @@ const CurrentUserPage: React.FC<Props> = ({ personalityInfo }) => {
);
}
if (mounted && (connecting || fetching)) {
if (connecting || fetching) {
return (
<Flex align="center" justify="center" h="100vh">
<Tooltip hasArrow label={connecting ? 'Connecting…' : 'Fetching User…'}>
<Center h="100vh">
<Stack align="center">
<Text fontSize="xl">
{connecting ? 'Connecting…' : 'Fetching User…'}
</Text>
<Spinner thickness="6px" color="whiteAlpha" size="xl" />
</Tooltip>
</Flex>
</Stack>
</Center>
);
}
if (user) {
return <PlayerPage player={user} personalityInfo={personalityInfo} />;
return <PlayerPage player={user} />;
}
if (error) {
return (
<Text textAlign="center" mt="25vh">
Error Loading User: <q>{error.message}</q>
</Text>
<Center h="100vh">
<Stack align="center">
<Text>
Error Loading User: <q>{error.message}</q>
</Text>
<MetaButton onClick={connect}>Try Again</MetaButton>
</Stack>
</Center>
);
}

View File

@@ -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<Props> = ({
player,
personalityInfo,
}): ReactElement => {
export const PlayerPage: React.FC<Props> = ({ 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 <LoadingState />;
@@ -87,23 +100,24 @@ export const PlayerPage: React.FC<Props> = ({
if (!player) return <Page404 />;
return (
<PageContainer p={0} px={[0, 4, 8]}>
<PageContainer pt={0} px={[0, 4, 8]}>
<HeadComponent
title={`MetaGame Player Profile: ${getPlayerName(player)}`}
title={`MetaGame Profile: ${getPlayerName(player)}`}
description={(getPlayerDescription(player) ?? '').replace('\n', ' ')}
url={getPlayerURL(player, { rel: false })}
img={getPlayerImage(player)}
/>
<Box
bg={`url(${getPlayerBannerFull(player)}) no-repeat`}
bg={`url(${banner}) no-repeat`}
bgSize="cover"
bgPos="center"
h={72}
pos="absolute"
w="full"
top={0}
/>
<Flex w="full" h="full" pt="3rem" direction="column" align="center">
<Grid {...{ player, personalityInfo }} />
<Flex w="full" h="full" pt={12} direction="column" align="center">
<Grid {...{ player }} />
</Flex>
</PageContainer>
);
@@ -111,34 +125,38 @@ export const PlayerPage: React.FC<Props> = ({
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<Maybe<HTMLElement>>) => {
const [heights, setHeights] = useState<Record<string, number>>({});
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<string, number> = {};
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<Props> = ({
player: initPlayer,
personalityInfo,
}): ReactElement => {
export const Grid: React.FC<Props> = ({ 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<boolean>(false);
const [exitAlertReset, setExitAlertReset] = useState<boolean>(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<Array<Maybe<HTMLElement>>>([]);
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<ProfileLayoutData>(
() =>
@@ -199,17 +204,13 @@ export const Grid: React.FC<Props> = ({
layouts: currentLayouts,
} = currentLayoutData;
const itemsRef = useRef<HTMLElement[]>([]);
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<Props> = ({
}
}, [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<Props> = ({
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<Layout>, 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<Props> = ({
);
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<Props> = ({
[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<Props> = ({
<Box
className="gridWrapper"
width="100%"
height="100%"
sx={wrapperSX}
maxW="96rem"
pb="3rem"
mb="12rem"
pt={isOwnProfile ? 0 : '4rem'}
>
{isOwnProfile && (
<ButtonGroup
w="100%"
px="2rem"
w="full"
mb={4}
px={8}
justifyContent="end"
variant="ghost"
zIndex={10}
h="3rem"
mb="1rem"
isAttached
size={mobile ? 'xs' : 'md'}
>
{changed && canEdit && !isDefaultLayout && (
{changed && editing && !isDefaultLayout && (
<MetaButton
aria-label="Reset"
aria-label="Reset Layout"
_hover={{ background: 'purple.600' }}
textTransform="uppercase"
px={12}
px={[8, 12]}
letterSpacing="0.1em"
size="lg"
fontSize="sm"
onClick={() => setExitAlertReset(true)}
leftIcon={<RepeatClockIcon />}
whiteSpace="pre-wrap"
leftIcon={mobile ? undefined : <RepeatClockIcon />}
>
Reset
</MetaButton>
)}
{changed && canEdit && (
{editing && (
<MetaButton
aria-label="Cancel edit layout"
aria-label="Cancel Layout Edit"
colorScheme="purple"
_hover={{ background: 'purple.600' }}
textTransform="uppercase"
px={12}
px={[9, 12]}
letterSpacing="0.1em"
size="lg"
fontSize="sm"
onClick={() => setExitAlertCancel(true)}
leftIcon={<CloseIcon />}
leftIcon={mobile ? undefined : <CloseIcon />}
>
Cancel
</MetaButton>
)}
<ConfirmModal
isOpen={exitAlertReset}
onNope={() => 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?"
/>
<ConfirmModal
isOpen={exitAlertCancel}
@@ -392,32 +390,31 @@ export const Grid: React.FC<Props> = ({
onYep={handleCancel}
header="Are you sure you want to cancel editing the layout?"
/>
<MetaButton
aria-label="Edit layout"
borderColor="transparent"
background="rgba(17, 17, 17, 0.9)"
_hover={{ color: 'white', borderColor: 'transparent' }}
variant="outline"
textTransform="uppercase"
px={12}
letterSpacing="0.1em"
size="lg"
fontSize="sm"
bg="transparent"
color={canEdit ? 'red.400' : 'pinkShadeOne'}
leftIcon={<EditIcon />}
transition="color 0.2s ease"
isLoading={saving || fetchingSaveRes}
onClick={toggleEditLayout}
>
<ResponsiveText
content={{
base: canEdit ? 'Save' : 'Edit',
md: `${canEdit ? 'Save' : 'Edit'} layout`,
}}
/>
</MetaButton>
{(!editing || changed) && (
<MetaButton
aria-label="Edit Layout"
borderColor="transparent"
background="rgba(17, 17, 17, 0.9)"
_hover={{ color: 'white' }}
variant="outline"
textTransform="uppercase"
px={[5, 12]}
letterSpacing="0.1em"
bg="transparent"
color={editing ? 'red.400' : 'pinkShadeOne'}
leftIcon={mobile ? undefined : <EditIcon />}
transition="color 0.2s ease"
isLoading={saving || updating}
onClick={toggleEditLayout}
>
<ResponsiveText
content={{
base: editing ? 'Save' : 'Edit Layout ',
md: `${editing ? 'Save' : 'Edit'} Layout`,
}}
/>
</MetaButton>
)}
</ButtonGroup>
)}
<ResponsiveGridLayout
@@ -427,7 +424,7 @@ export const Grid: React.FC<Props> = ({
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<Props> = ({
sm: [20, 20],
}}
>
{currentLayoutItems.map(({ boxKey, boxType, boxMetadata }, i) => (
<Flex key={boxKey} className="gridItem" id={boxKey}>
{boxType === BoxType.PLAYER_ADD_BOX ? (
{currentLayoutItems.map(({ key, type, metadata }, i) => (
<Flex {...{ key }} className="gridItem" id={key}>
{type === BoxTypes.PLAYER_ADD_BOX ? (
<PlayerAddSection
boxList={availableBoxList}
{...{ player, onAddBox, personalityInfo }}
ref={(e) => {
itemsRef.current[i] = e as HTMLElement;
boxes={availableBoxes}
{...{ player, onAddBox }}
ref={(e: Maybe<HTMLElement>) => {
itemsRef.current[i] = e;
}}
/>
) : (
<PlayerSection
{...{
boxType,
boxMetadata,
type,
metadata,
player,
isOwnProfile,
personalityInfo,
canEdit,
editing,
onRemoveBox,
}}
ref={(e) => {
itemsRef.current[i] = e as HTMLElement;
ref={(e: Maybe<HTMLElement>) => {
itemsRef.current[i] = e;
}}
/>
)}
@@ -476,12 +472,19 @@ export const Grid: React.FC<Props> = ({
type QueryParams = { username: string };
export const getStaticPaths: GetStaticPaths<QueryParams> = 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,

View File

@@ -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<typeof getStaticProps>;
const AvailabilitySetup: React.FC<DefaultSetupProps> = () => {
const { user } = useUser();
const [available, setAvailability] = useState<Maybe<number>>(
user?.profile?.availableHours ?? null,
);
if (user) {
if (user.profile?.availableHours != null && available === null) {
setAvailability(user.profile.availableHours);
}
}
return (
<SetupContextProvider>
<SetupProfile>
<SetupAvailability {...{ available, setAvailability }} />
</SetupProfile>
</SetupContextProvider>
);
};
const AvailabilitySetup: React.FC<DefaultSetupProps> = () => (
<SetupContextProvider>
<SetupProfile>
<SetupAvailability />
</SetupProfile>
</SetupContextProvider>
);
export default AvailabilitySetup;

View File

@@ -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<typeof getStaticProps>;
const PersonalityTypeSetup: React.FC<DefaultSetupProps> = () => (
const ColorDispositionSetup: React.FC<DefaultSetupProps> = () => (
<SetupContextProvider>
<SetupProfile>
<SetupPersonalityType />
<SetupColorDisposition />
</SetupProfile>
</SetupContextProvider>
);
export default PersonalityTypeSetup;
export default ColorDispositionSetup;

View File

@@ -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<typeof getStaticProps>;
const PronounsSetup: React.FC<DefaultSetupProps> = () => {
const [pronouns, setPronouns] = useState<string>();
const { user } = useUser();
const PronounsSetup: React.FC<DefaultSetupProps> = () => (
<SetupContextProvider>
<SetupProfile>
<SetupPronouns />
</SetupProfile>
</SetupContextProvider>
);
if (user?.profile?.pronouns && pronouns === undefined) {
setPronouns(user.profile.pronouns);
}
return (
<SetupContextProvider>
<SetupProfile>
<SetupPronouns {...{ pronouns, setPronouns }} />
</SetupProfile>
</SetupContextProvider>
);
};
export default PronounsSetup;

View File

@@ -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<typeof getStaticProps>;
const PlayerRolesSetup: React.FC<Props> = ({ roleChoices }) => (
const PlayerRolesSetup: React.FC<Props> = ({ choices }) => (
<SetupContextProvider>
<SetupProfile>
<SetupRoles {...{ roleChoices }} />
<SetupRoles {...{ choices }} />
</SetupProfile>
</SetupContextProvider>
);

View File

@@ -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<typeof getStaticProps>;
const UsernameSetup: React.FC<DefaultSetupProps> = () => {
const [username, setUsername] = useState<string>();
const { address } = useWeb3();
const { user } = useUser();
const { username: name } = user?.profile ?? {};
const UsernameSetup: React.FC<DefaultSetupProps> = () => (
<SetupContextProvider>
<SetupProfile>
<SetupUsername />
</SetupProfile>
</SetupContextProvider>
);
if (
name &&
name.toLowerCase() !== address?.toLowerCase() &&
username === undefined
) {
setUsername(name);
}
return (
<SetupContextProvider>
<SetupProfile>
<SetupUsername {...{ username, setUsername }} />
</SetupProfile>
</SetupContextProvider>
);
};
export default UsernameSetup;

View File

@@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1C3.13406 1 0 3.68644 0 6.99978C0 7.41433 0.049 7.81917 0.142722 8.21C0.834556 9.40039 1.31328 8.64906 3.5 7.5555C5.72717 6.44172 3.5 9.11106 2.72222 10.6666C2.48306 11.1449 2.61256 11.5498 2.94 11.8815C4.08567 12.5831 5.48489 12.9999 7 12.9999C10.8659 12.9999 14 10.3139 14 6.99978C14 3.68644 10.8659 1 7 1ZM8.10794 10.5072C7.95433 11.0306 7.15128 11.2554 6.314 11.0092C5.47672 10.7634 4.92256 10.1404 5.07617 9.61661C5.22978 9.09317 6.03283 8.86839 6.87011 9.11494C7.70739 9.36033 8.26156 9.98333 8.10794 10.5072Z" fill="#D99E82"/>
<path d="M3.88883 5.22201C4.53317 5.22201 5.0555 4.69967 5.0555 4.05534C5.0555 3.41101 4.53317 2.88867 3.88883 2.88867C3.2445 2.88867 2.72217 3.41101 2.72217 4.05534C2.72217 4.69967 3.2445 5.22201 3.88883 5.22201Z" fill="#5C913B"/>
<path d="M7.77775 4.44417C8.42208 4.44417 8.94442 3.92184 8.94442 3.27751C8.94442 2.63317 8.42208 2.11084 7.77775 2.11084C7.13342 2.11084 6.61108 2.63317 6.61108 3.27751C6.61108 3.92184 7.13342 4.44417 7.77775 4.44417Z" fill="#226699"/>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9057 13.9998L12.8889 13.6109C12.8889 12.0553 11.739 10.8887 9.79454 10.8887H5.12785C3.18339 10.8887 2 12.0553 2 13.6109L2.01672 13.9998H12.9057Z" fill="#F2760F"/>
<path d="M3.94434 11.0046V13.9998H5.11101V10.8887C4.68206 10.8887 4.29356 10.9287 3.94434 11.0046ZM9.7777 10.8887V13.9998H10.9444V11.0046C10.5951 10.9287 10.2066 10.8887 9.7777 10.8887Z" fill="#FFF75F"/>
<path d="M5.5 10.5H9.38891V12.0556H5.5V10.5Z" fill="#292F33"/>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,3 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6522 12.9002L1.09977 0.347812C0.49505 -0.257292 0 -0.0523502 0 0.803196V12.4445C0 13.3 0.699992 14 1.55554 14H13.1968C14.0524 14 14.2573 13.5053 13.6522 12.9002ZM6.59821 10.8889H3.88885C3.46107 10.8889 3.11108 10.5389 3.11108 10.1112V7.4014C3.11108 6.97363 3.3588 6.87135 3.66096 7.17352L6.82609 10.339C7.12826 10.6412 7.02559 10.8889 6.59821 10.8889Z" fill="#FFCC4D"/>
</svg>

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 451 B

View File

@@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path d="M1.05778 2.1084H2.43056V3.15023H1.736C1.04144 3.15023 1.04144 3.49751 1.04144 3.84479V6.71751C1.04144 7.24601 1.54272 7.31718 1.54272 7.31718H2.43056V8.35901H1.05778C0.473667 8.35901 0 7.88573 0 7.30162V3.16579C0 2.58206 0.473278 2.1084 1.05778 2.1084Z" fill="#FFAC33"/>
<path d="M2.08325 2.1084H7.9862V9.31684C7.9862 9.74462 7.6362 10.0946 7.20842 10.0946H2.86103C2.43325 10.0946 2.08325 9.74462 2.08325 9.31684V2.1084Z" fill="#FFAC33"/>
<path d="M3.29853 9.05279C3.49025 9.05279 3.64581 8.89762 3.64581 8.70551V4.88623C3.64581 4.69451 3.49064 4.53895 3.29853 4.53895C3.10681 4.53895 2.95125 4.69412 2.95125 4.88623V8.70551C2.95125 8.89762 3.10681 9.05279 3.29853 9.05279ZM5.03453 9.05279C5.22625 9.05279 5.38181 8.89762 5.38181 8.70551V4.88623C5.38181 4.69451 5.22664 4.53895 5.03453 4.53895C4.84281 4.53895 4.68725 4.69412 4.68725 4.88623V8.70551C4.68764 8.89762 4.84281 9.05279 5.03453 9.05279ZM2.08325 3.31823V2.1084H7.9862V4.0054C7.87264 4.04701 7.75131 4.07345 7.62336 4.07345C7.18081 4.07345 6.80242 3.80084 6.64492 3.41468C6.55236 3.49479 6.43297 3.54456 6.30153 3.54456C6.12264 3.54456 5.96514 3.45512 5.86947 3.31901C5.68164 3.61301 5.35459 3.80901 4.9797 3.80901C4.63592 3.80901 4.33297 3.64218 4.14009 3.38784C4.04364 3.63401 3.80564 3.80901 3.52564 3.80901C3.27481 3.80901 3.05936 3.66745 2.94697 3.46173C2.84197 3.51307 2.72531 3.54456 2.60009 3.54456C2.25709 3.54456 2.08325 3.31823 2.08325 3.31823Z" fill="#F4900C"/>

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2237 9.30922C11.9911 6.73556 10.7078 5.44444 10.7078 5.44444L8.37446 2.33333H5.26335L2.93001 5.44444C2.93001 5.44444 2.37818 6.00172 1.93174 7.10694C1.15824 7.36361 0.59668 8.08461 0.59668 8.94444C0.59668 9.50756 0.839735 10.0108 1.2224 10.3658C1.07618 10.6388 0.985569 10.9461 0.985569 11.2778C0.985569 12.0392 1.42735 12.6918 2.06474 13.0107C2.56368 13.7581 3.23024 14 3.70779 14H9.93001C10.4663 14 11.2417 13.6971 11.7492 12.7128C12.5002 12.4441 13.0411 11.7328 13.0411 10.8889C13.0411 10.2363 12.7168 9.66233 12.2237 9.30922ZM6.8189 2.33333C7.03279 2.33333 7.23035 2.27189 7.40224 2.17156C7.57451 2.27189 7.77207 2.33333 7.98557 2.33333C8.62996 2.33333 9.54112 1.42178 9.54112 0.777778C9.54112 0.777778 9.54112 0 8.76335 0C8.4569 0 8.37446 0.388889 7.98557 0.388889C7.59668 0.388889 7.59668 0 6.8189 0C6.04112 0 6.04112 0.388889 5.65224 0.388889C5.26335 0.388889 5.18129 0 4.87446 0C4.09668 0 4.09668 0.777778 4.09668 0.777778C4.09668 1.42178 5.00824 2.33333 5.65224 2.33333C5.86574 2.33333 6.06329 2.27189 6.23557 2.17156C6.40785 2.27189 6.6054 2.33333 6.8189 2.33333Z" fill="#FDD888"/>
<path d="M9.15226 2.33322C9.15226 2.54789 8.97843 2.72211 8.76337 2.72211H4.87448C4.65982 2.72211 4.4856 2.54789 4.4856 2.33322C4.4856 2.11856 4.65982 1.94434 4.87448 1.94434H8.76337C8.97843 1.94434 9.15226 2.11856 9.15226 2.33322Z" fill="#BF6952"/>
<path d="M9.11366 9.54417C9.11366 7.80311 5.77661 7.91667 5.77661 6.86628C5.77661 6.35761 6.28255 6.10911 6.86977 6.10911C7.85677 6.10911 8.03255 6.71928 8.47939 6.71928C8.79555 6.71928 8.948 6.52755 8.948 6.3125C8.948 5.81317 8.16089 5.43517 7.40605 5.3045V4.82228C7.40605 4.52167 7.15327 4.27783 6.84061 4.27783C6.52755 4.27783 6.27439 4.52167 6.27439 4.82228V5.32122C5.4515 5.50128 4.74333 6.05039 4.74333 6.94522C4.74333 8.61705 8.07961 8.54939 8.07961 9.72383C8.07961 10.131 7.6215 10.5378 6.86977 10.5378C5.74161 10.5378 5.36594 9.80316 4.90783 9.80316C4.68461 9.80316 4.48511 9.98361 4.48511 10.2558C4.48511 10.6887 5.23877 11.209 6.27516 11.3541L6.27477 11.3579V11.9016C6.27477 12.2018 6.52833 12.4461 6.841 12.4461C7.15366 12.4461 7.40683 12.2018 7.40683 11.9016V11.3579C7.40683 11.3513 7.40372 11.3463 7.40333 11.3404C8.33589 11.1732 9.11366 10.5891 9.11366 9.54417Z" fill="#67757F"/>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path d="M5.44442 7.05566C4.15564 7.05566 3.11108 8.10061 3.11108 9.389C3.11108 10.6774 4.15564 11.7223 5.44442 11.7223C6.73319 11.7223 7.77775 10.6774 7.77775 9.389C7.77775 8.10061 6.73281 7.05566 5.44442 7.05566ZM5.44442 10.9446C4.58536 10.9446 3.88886 10.2481 3.88886 9.389C3.88886 8.52994 4.58536 7.83344 5.44442 7.83344C6.30347 7.83344 6.99997 8.52994 6.99997 9.389C6.99997 10.2481 6.30347 10.9446 5.44442 10.9446Z" fill="#3B88C3"/>
<path d="M0.693389 5.12007V5.12785C0.304111 5.2134 0 5.86518 0 6.66668C0 7.46818 0.304111 8.11996 0.693389 8.20512V8.21329L11.8549 11.3135V2.0249L0.693389 5.12007Z" fill="#55ACEE"/>
<path d="M12.0555 11.3333C13.1294 11.3333 14 9.244 14 6.66667C14 4.08934 13.1294 2 12.0555 2C10.9816 2 10.1111 4.08934 10.1111 6.66667C10.1111 9.244 10.9816 11.3333 12.0555 11.3333Z" fill="#226699"/>

Before

Width:  |  Height:  |  Size: 923 B

After

Width:  |  Height:  |  Size: 888 B

View File

@@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path d="M1.52946 12.7622C1.64429 12.8278 1.77565 12.8687 1.91762 12.8687C2.35219 12.8687 2.70418 12.5164 2.70418 12.0822C2.70418 11.648 2.35219 11.2956 1.91762 11.2956C1.48305 11.2956 1.13107 11.648 1.13107 12.0822C1.13107 12.2242 1.17157 12.3555 1.23725 12.4704L0 13.7076L0.344511 10.9024L3.88401 8.14941L5.8504 10.1158L3.09746 13.6553L0.291812 13.9998L1.52946 12.7622Z" fill="#99AAB5"/>
<path d="M8.74379 1.97829C8.29113 2.43095 8.29113 3.16441 8.74379 3.61707L10.3826 5.25626C10.8352 5.70852 11.5695 5.70852 12.0214 5.25626L13.6602 3.61707C14.1124 3.16441 14.1124 2.43095 13.6602 1.97829L12.0214 0.339497C11.5691 -0.113166 10.8352 -0.113166 10.3826 0.339497L8.74379 1.97829Z" fill="#66757F"/>
<path d="M3.63125 7.09115L10.3964 0.325195L13.6736 3.60316L6.90882 10.3687C6.45655 10.821 5.7227 10.821 5.26964 10.3687L3.63125 8.72954C3.17858 8.27688 3.17858 7.54342 3.63125 7.09115Z" fill="#31373D"/>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -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<typeof BoxTypes>;
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<string, string> = {},
) => `${type}-${hashCode(JSON.stringify(metadata))}`;
export const getBoxTypeFromKey = (boxKey: string): BoxType =>
export const getBoxKey = (target: Maybe<HTMLElement>) =>
(target?.offsetParent as HTMLElement)?.offsetParent?.id;
export const getBoxType = (boxKey: string): BoxType =>
boxKey.split('-').slice(0, -1).join('-') as BoxType;

View File

@@ -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()}`;

View File

@@ -19,20 +19,17 @@ export function findHighLowPrice(
export function volumeChange(
vols: Array<Array<number>>,
todayVol: Record<string, number>,
{ usd: today }: Record<string, number>,
): number {
const plots = [];
let element: Array<number> = [];
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 = {

View File

@@ -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<Layout> = {},
): 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<string, number>,
): 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) {

View File

@@ -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&shy;fess&shy;ional</>,
lg: <>2. Pro&shy;fess&shy;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<string>): number {
return this.steps.findIndex((step) => step.slug === slug);
}

View File

@@ -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!

View File

@@ -1,7 +1,7 @@
{
"org": "MetaFam",
"repo": "TheGame",
"tokenAddress": "0x30cF203b48edaA42c3B4918E955fED26Cd012A3F",
"tokenAddress": "0xeaecc18198a475c921b24b8a6c1c1f0f5f3f7ea0",
"labels": ["RoadMap"],
"votingMethod": "QUADRATIC",
"chainId": 1

Some files were not shown because too many files have changed in this diff Show More