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>
15
.prettierrc.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
printWidth: 80,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'always',
|
||||
// semi: false,
|
||||
overrides: [
|
||||
{
|
||||
files: '*.yaml',
|
||||
options: {
|
||||
singleQuote: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "always",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.yaml",
|
||||
"options": {
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
180
CHANGELOG.md
@@ -2,6 +2,186 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## 0.2.0 (2022-02-28)
|
||||
|
||||
### Features
|
||||
|
||||
- add <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
|
||||
|
||||
@@ -141,9 +141,7 @@ type DiscordGuildAuthResponse {
|
||||
}
|
||||
|
||||
type CacheProcessOutput {
|
||||
success : Boolean!
|
||||
queued : Boolean!
|
||||
error : String
|
||||
updateIDXProfile : uuid
|
||||
}
|
||||
|
||||
type ExpiredPlayerProfiles {
|
||||
|
||||
@@ -36,7 +36,6 @@ actions:
|
||||
definition:
|
||||
kind: asynchronous
|
||||
handler: '{{ACTION_BASE_ENDPOINT}}/idxCache/updateSingle'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: player
|
||||
- role: public
|
||||
|
||||
@@ -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
|
||||
;
|
||||
@@ -0,0 +1,6 @@
|
||||
INSERT INTO profile (player_id)
|
||||
SELECT id FROM player
|
||||
WHERE id NOT IN (
|
||||
SELECT player_id FROM profile
|
||||
)
|
||||
;
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ export const typeDefs = gql`
|
||||
chain: String!
|
||||
title: String
|
||||
version: String
|
||||
avatarUrl: String
|
||||
avatarURL: String
|
||||
}
|
||||
|
||||
type Member {
|
||||
|
||||
@@ -16,8 +16,7 @@ export const MetaButton: React.FC<
|
||||
fontSize="sm"
|
||||
bg="purple.400"
|
||||
color="white"
|
||||
{...{ ref }}
|
||||
{...props}
|
||||
{...{ ref, ...props }}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
28
packages/design-system/src/ViewAllButton.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
BIN
packages/web/assets/cursive-title-small.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
packages/web/assets/cursive-title.png
Normal file
|
After Width: | Height: | Size: 462 KiB |
6
packages/web/assets/discord.svg
Normal 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 |
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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}</>;
|
||||
};
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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­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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 haven’t ' : 'This player hasn’t '}
|
||||
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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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”
|
||||
& 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 &{' '}
|
||||
Ideally, it would be WETH &
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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­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­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: 'It’s not possible to be available for negative time.',
|
||||
},
|
||||
max: {
|
||||
value: 24 * 7,
|
||||
message: `There’s 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>
|
||||
);
|
||||
};
|
||||
|
||||
217
packages/web/components/Setup/SetupColorDisposition.tsx
Normal 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­po­sit­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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
<
|
||||
</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">
|
||||
>
|
||||
</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>
|
||||
|
||||
@@ -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­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>
|
||||
);
|
||||
|
||||
@@ -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­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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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> </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>
|
||||
)}
|
||||
|
||||
@@ -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­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­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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
260
packages/web/components/Setup/WizardPane.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export const InsertCacheInvalidation = /* GraphQL */ `
|
||||
mutation InsertCacheInvalidation($playerId: uuid!) {
|
||||
updateIDXProfile(playerId: $playerId) {
|
||||
success
|
||||
}
|
||||
updateIDXProfile(playerId: $playerId)
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ export const useSaveCeramicProfile = ({
|
||||
useProfileField({
|
||||
field: key,
|
||||
player: user,
|
||||
owner: true,
|
||||
});
|
||||
return [key, setter];
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)` });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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()}`;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Maybe } from '@metafam/utils';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export type SetupStep = {
|
||||
label: string;
|
||||
slug?: string;
|
||||
@@ -7,7 +10,7 @@ export type SetupStep = {
|
||||
export type SetupSection = {
|
||||
label: string;
|
||||
title: {
|
||||
[any: string]: string | undefined;
|
||||
[any: string]: string | undefined | ReactElement;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,8 +24,8 @@ export class SetupOptions {
|
||||
label: 'Professional Profile',
|
||||
title: {
|
||||
base: 'Pro',
|
||||
sm: '2. Professional',
|
||||
lg: '2. Professional Profile',
|
||||
sm: <>2. Pro­fess­ional</>,
|
||||
lg: <>2. Pro­fess­ional Profile</>,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -50,8 +53,8 @@ export class SetupOptions {
|
||||
sectionIndex: 0,
|
||||
},
|
||||
{
|
||||
label: 'Personality Type',
|
||||
slug: 'personalityType',
|
||||
label: 'Color Disposition',
|
||||
slug: 'colorDisposition',
|
||||
sectionIndex: 0,
|
||||
},
|
||||
{
|
||||
@@ -96,7 +99,7 @@ export class SetupOptions {
|
||||
},
|
||||
];
|
||||
|
||||
stepIndexMatchingSlug(slug: string | null): number {
|
||||
stepIndexMatchingSlug(slug: Maybe<string>): number {
|
||||
return this.steps.findIndex((step) => step.slug === slug);
|
||||
}
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"org": "MetaFam",
|
||||
"repo": "TheGame",
|
||||
"tokenAddress": "0x30cF203b48edaA42c3B4918E955fED26Cd012A3F",
|
||||
"tokenAddress": "0xeaecc18198a475c921b24b8a6c1c1f0f5f3f7ea0",
|
||||
"labels": ["RoadMap"],
|
||||
"votingMethod": "QUADRATIC",
|
||||
"chainId": 1
|
||||
|
||||