diff --git a/.eslintrc.json b/.eslintrc.json index 8e053c1..8e54846 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,8 +23,7 @@ // "ecmaFeatures": { // "jsx": true // }, - "ecmaVersion": 2021, - // "presets": ["@babel/preset-react"], + "ecmaVersion": "latest", "requireConfigFile": false, "sourceType": "module" }, @@ -40,14 +39,17 @@ "comma-dangle": "off", "indent": "off", "max-len": [ - "error", + "warn", { + "code": 100, "ignoreComments": true, "ignoreUrls": true, "ignoreStrings": true, - "ignoreTemplateLiterals": true + "ignoreTemplateLiterals": true, + "tabWidth": 2 } ], + "no-undef": "error", "radix": ["error", "as-needed"], "react/no-unescaped-entities": "off", "react/prop-types": "off", diff --git a/.firebase/hosting.YnVpbGQ.cache b/.firebase/hosting.YnVpbGQ.cache index 671c6f1..f38d002 100644 --- a/.firebase/hosting.YnVpbGQ.cache +++ b/.firebase/hosting.YnVpbGQ.cache @@ -7,102 +7,102 @@ images/icon.svg,1641761281189,c63d6f3f53a506c036c277d7520b07d3acd52f5fe001bc90c2 images/Manifest.jpg,1624659695932,6bcf0d8d0cf7134a8562146d9074ab3f68e4b1f6984578f40fde56118b6a6a95 images/Morph.jpg,1624659695935,84d08f0ec74e073b6000f6189aba277926506bc980eb2e46d1fdac5d5add5d53 images/MTG Card Back.jpg,1624659695928,f5b5263fde76fc97900f3225ada342b9cd079206f793c4dada18f20695a514cc -asset-manifest.json,1643427073638,bcf0e0cb7aeadb45b08071cb00f1c46a48c9b7384f505c232795216955df3def -index.html,1643427073637,ac7f8b627ef62861b3d89e235cc74aec6ff0be0225a18478e8b33fed3932c6a2 -static/js/0.093c8872.chunk.js,1643427073613,2a85d9c77b7813914c7bd268ea33046242a1660df60dae12217572db789d9b92 -static/js/1.f4b4b762.chunk.js,1643427073614,949233467676c6e111100408349840bed5855eb0c4cce40ffce4ba46d3633413 -static/js/10.a7f69662.chunk.js,1643427073619,f8eb616f8facc83af0ce6daae5c61474eba24728f166a77e3cf8bd10099db095 -static/js/11.8b9f6187.chunk.js,1643427073619,3119a789cf2c32bcc4c93b0021b1462e9e6c77d82609914b76f45ba4265ab99a -static/js/0.093c8872.chunk.js.map,1643427073630,cb3fac0313eb69f6de1669773510741ad69b030d75dde62e5898218e7b18e1ad -static/js/12.2090bdaf.chunk.js,1643427073619,66d3286f48c3081a83b7d15b88bc07450dcfda1ebd371935153bbc3925aaee30 -static/js/14.2097316b.chunk.js,1643427073620,9742de4b3c119afcd40238a82540a4658a02b4effd2d23fc3b74d616838ada74 -static/js/10.a7f69662.chunk.js.map,1643427073635,682a5980d7db217626431f76ac296ec557c0a5344f352d43b1f72a27daf8cd65 -static/js/11.8b9f6187.chunk.js.map,1643427073637,07ed7ac3a92768f8f86a6194a8ac6f0dd126d7b59e4ff48784c12304491d6ed4 -static/js/12.2090bdaf.chunk.js.map,1643427073637,1a0fd6c2737fb01e78cdd82d4cc477a25c75913518b86c2516ca11a7c1aedd98 -static/js/13.13e95614.chunk.js,1643427073619,5bf385b82cb5ccda358296a5af7a05af7ad8a58ad5f360f7fb0e657e28e70d0e -static/js/15.622e9520.chunk.js,1643427073620,751530d680c637ece9bc7593b92d9e1978a952828179d1a01df98e52580df2f2 -static/js/15.622e9520.chunk.js.map,1643427073639,1f1183b216c07a80ecbbd50c996dcea064faf8c7f372d9f06c1836307fd838da -static/js/16.b4dab356.chunk.js,1643427073620,7b8616d2478020312e8cec3d96c0a57134703ed4472cab4c9cc16dae42da87b3 -static/js/17.224c798a.chunk.js,1643427073620,a191d48423669c5710f2928a1b72e97837f61d457496b8965d6ddd45068fed84 -static/js/18.bb891cb1.chunk.js,1643427073620,d566b72384953c82b87c774328d9b435961e4bd6c698942e467771a42f4aa8d1 -static/js/17.224c798a.chunk.js.map,1643427073639,48e6c9efa030f608c5add530fb7fd2a0770b2b01fd1c9667c25e98711d606d00 -static/js/19.ca88c77e.chunk.js,1643427073621,437293638a7d8a5d387285996b07164c8ea4ca8ec1f785e578b12ad12d7021d4 -static/js/18.bb891cb1.chunk.js.map,1643427073640,2149db7d9ec1861352b5f48b48ea221be0cb041f60a3407f5ed039a86af6b508 -static/js/19.ca88c77e.chunk.js.map,1643427073640,4413388ce9b7c840c809d3a273608bdbdc070a74d45c549df49270a6b090216a -static/js/16.b4dab356.chunk.js.map,1643427073639,29ddf687366930a584fc0a515a9d88e99556bf33642aa6b0fc751bf11fd4860b -static/js/5.1086426c.chunk.js.LICENSE.txt,1643427073630,efc545aa142660a7bc9f4755a386ff5377fa19b279ccf9006b03ebdff35ce7c8 -static/js/2.1bbe3631.chunk.js,1643427073614,cb7eced94dc4d24c4f227a12f95dbf52bcd04bbbac54b8aee2647fe95c2366fd -static/js/6.69308370.chunk.js.LICENSE.txt,1643427073630,a2e7f497cf638d332c05a0a3902858b24ebf118dd2dd6415e42a976f3eb4eef3 -static/js/main.14f7d9fa.chunk.js,1643427073614,2becc01edd9d25218fe8ce2fefbed8203d3e8eee8cf0b59712742044ba322297 -static/js/runtime-main.76c18f9a.js,1643427073614,1a1967884e8f7b41fb55fa15da2998d7bc7c60f668c4ae12a3e8fa398ac6380c -static/media/0-mana-symbol.7d5443ee.svg,1643427073600,4dd9883b97ceff8afac0b50c81abca34d4a08224eb8f8c748e0b6ffcab18cfb4 -static/media/1-mana-symbol.34b61e5d.svg,1643427073600,5adc6117dc24a2862f0bb07e58b7e1516809a05e4c395814fb68069930e0eae8 -static/media/10-mana-symbol.595ceb65.svg,1643427073603,921cbd64cd8607b2242df1490a7abd48132014ef64b46fa24527053339336b0a -static/media/11-mana-symbol.214c7717.svg,1643427073604,929f77690c6f6300c587e0c20a26636b432f699c9bccd72c700eb141ed2029b1 -static/media/12-mana-symbol.e5cdbe96.svg,1643427073604,ea5b6768c895ab0244954f7c2adf7248f134a0edc05187294e048e6e97ba1b4f -static/js/runtime-main.76c18f9a.js.map,1643427073632,1e00de16a136e432e47a881b0d847ecb0dd338f8dce8c8e3a8635f27385e3fc6 -static/media/13-mana-symbol.d75cb718.svg,1643427073604,70a924eccefde61ab088be9b1b3033c318560e4a38c0b4451cdb33d705248e99 -static/js/14.2097316b.chunk.js.map,1643427073639,bca21667b5dc7a0c3aef7c541e710eb886761753c82fb8eff5de6d2ad5db25a1 -static/js/1.f4b4b762.chunk.js.map,1643427073631,a2bf2064e7503ad174a268d7f1f32f9837cb63267701d4465deb3da08265c0a0 -static/js/main.14f7d9fa.chunk.js.map,1643427073632,664596233a034fa42103176194ceb66d6257b68f420f71b66b57a1771d75c221 -static/media/14-mana-symbol.9f239954.svg,1643427073605,a9d5f7899f0da43c688efb446d77d81435ebbbdc115ea99a4d1b1c03a4a2bbf0 -static/media/15-mana-symbol.a4113def.svg,1643427073607,c837d5f28afc1f802b0212f1af40167ed1bbc600a741d793c046e37364174bda -static/media/16-mana-symbol.48c7fbf1.svg,1643427073607,e40534b9151717cde9ed428c74a13241134fa1c5f1a319a32b53d6487b9cde9c -static/media/2-mana-symbol.f61762ff.svg,1643427073602,bd849225c8e1da5fc431c84d12314d5b7992aae38006b512e6043c8f483d5ecc -static/media/2B-mana-symbol.376e4820.svg,1643427073608,6b2753f01b24080ec4a5cc1be5e43456c53ddeedeea196fa1f77793cd5872ea1 -static/media/2G-mana-symbol.0e214514.svg,1643427073608,31e6d569719ca61f321f438b4910fbfcabd7d984bc10afbaebc206dbf20ed7d4 -static/media/2R-mana-symbol.1647b1a2.svg,1643427073608,236b9486a41a607fa671bd520c23573b8302a2b963a3b5e0aa49073ca8b6f04e -static/media/2U-mana-symbol.b6b79ece.svg,1643427073608,0be114fea30996c3193163aa125975efaca3b66e3bcc726d8380de09fed37f47 -static/media/2W-mana-symbol.93a636f4.svg,1643427073608,a803ae613eee5def3d5bf151dba6a86b726947ad96cba14b6514f2ed860d24ac -static/media/3-mana-symbol.7c58f071.svg,1643427073602,033eb7bf73981a534fa6ba6f93cae9a562cdf43f2b1be7b9e1ad980b34dd3449 -static/media/4-mana-symbol.a15041e1.svg,1643427073602,8776931df9149e659ced87bfdf49bec32ad146fb21ca06f152f53dd7ef5b8a2e -static/media/5-mana-symbol.4284cca1.svg,1643427073602,1bd40d8ead0bb386f4fce9fbc2d59669df68dd41cf9e51ae1aa04f3d7af2f226 -static/media/6-mana-symbol.49f9776a.svg,1643427073602,d896ea6aa09011b9c19b7568510c4a4103f03695a195622ddd9b8d1403caccbb -static/media/7-mana-symbol.42118de3.svg,1643427073602,5a67299b7be04a0fce1831b1c403a4c9e6e9ad2266a36306a5a468812d553c37 -static/media/8-mana-symbol.763a5d17.svg,1643427073603,5108532c7e0cce5d9ca45247ecf82e37fc6cb91bda8fb8080f4c2684e13d25d2 -static/media/9-mana-symbol.eaf18626.svg,1643427073603,302ba7a7b72f007d61ca1570b11edd83f4fb7a05071a4df5fd6fcb5dd0671aad -static/media/B-mana-symbol.39b24312.svg,1643427073596,d4fa39430c5ab736cbbcd44383f6dabe738050f6288f13f561cc6312daff9c2f -static/media/BG-mana-symbol.52cfa11d.svg,1643427073596,00563109665daa76b60e53dc53bd3bcca3a44025497be673ae1d67c76a854dbb -static/media/BP-mana-symbol.ebe58931.svg,1643427073608,5acdd1f8d51c1eab596b8451b413bcebf5beb17240feaf6023cb3893ad3afb63 -static/media/BR-mana-symbol.2bceeba1.svg,1643427073596,daf8b9510464dfc513d105e5308567d8ade3655a6dff8d7decd23e69ce3a5a9c -static/media/C-mana-symbol.52b07652.svg,1643427073596,836f38b29a8c3a63c63a95d3456857d4bd97921afdc4efecb9eadcf967df9cdc -static/media/G-mana-symbol.e3b3fa06.svg,1643427073596,0de0a219ac7d78244c498577c2aff10d6b74fb11376781fcb12e9b7ba1937843 -static/media/GP-mana-symbol.608640d1.svg,1643427073608,cc8bad90dad93031d9e21a436e369f1f6d63ac12c37b964bf91f3dd6ac999979 -static/media/gs1.4e1196f3.svg,1643427073613,942b4616520b10ba660cd6bfbc3fe09b439cdf939d4dea4c2fe6546440cb9d3c -static/media/GU-mana-symbol.a55e2ec9.svg,1643427073596,ca7afa5f8fbd94b85c111be9d12ec5815b4c9b80a98feb4838372fae8c461eb6 -static/media/GW-mana-symbol.608fd370.svg,1643427073596,7f8c12de61b8b9de9b6d3d0bd03e1b6932bdff58b3a36588df0a10e8fd0b1cab -static/js/13.13e95614.chunk.js.map,1643427073639,1c6b223ab960fa9a05820217a7e3ed33c8d1aa857b76b9b029583f576c66ec83 -static/media/iko.7c0a7c62.svg,1643427073613,01d1d2659e0cd745e32adbee25fc0213b79940bc7af2fa46d8ab4c131c340743 -static/media/jmp.56233b78.svg,1643427073614,b4cdc40cbafad76ae12a48e242e56ab46cbbb4cc8691d8f2ed79f6808890a897 -static/media/magic.9aa2f9b0.svg,1643427073614,c10de79e6cf1b4d1f5172bdb145653bee7ead63e30316fc1394f70a16c834493 -static/media/mor.97fa323f.svg,1643427073613,c7cc2ed96e29fa40e8e62d7cc1c1576bb5a335786145edeb9b2578f891840ce5 -static/media/mrd.72c4d3e8.svg,1643427073613,e2e5ba6e63525cfbe4436b6ef663bdd5b9b3eb9cfbaba0053d7c65100e2ef21a -static/media/ori.99fce50b.svg,1643427073613,5c9a318a104ed15c8df38af6c064be0109b9fc26dc4d774d09298735b78b3588 -static/media/planeswalker.e1165b9b.svg,1643427073612,fec243b594f9a41ccac327bc39c06daaef67e3a9f2cd5c924f198efa653b9ff9 -static/media/R-mana-symbol.6938c172.svg,1643427073596,c7ef65e73667aa5cff2e5c952e8d96c9c0598e48a72537dc797546ad1665283f -static/media/RG-mana-symbol.3312e337.svg,1643427073596,24d24027f3bd5e5bf5032e4ca398f50d016e0321550246457fcbef9ad98ed5a9 -static/js/9.73ae37e1.chunk.js,1643427073619,dced9999def1f84735390ae0c66442d9cbb1541cac9dfa4d62c1bd92d4b15751 -static/media/RP-mana-symbol.6c1f176d.svg,1643427073608,86be684d7d553ffe9f6f83fde3992eadd02eab8e8f3231357127ea8688ccab37 -static/js/8.21183cbc.chunk.js,1643427073619,91c63996098e5a3c5ffb5c7267a3ecdff611f80d770dfe79cae50dfa7519b186 -static/js/2.1bbe3631.chunk.js.map,1643427073631,6d7602d6955e657a6b5024647856f8a8be3a58e71b1905a18b81769a7a05a026 -static/media/RW-mana-symbol.0f7771b2.svg,1643427073597,79688503fb15e217724ccb0652b13d3ae0ae88f43d5a91193757a20ecff12015 -static/js/7.acd1f312.chunk.js,1643427073619,f40476db03f9f17918e7a99681edc542c689464e7df77032533dbe33d5cf26dd -static/media/tsr.6bf3706f.svg,1643427073613,e9ff1c3b2971a4589a5c7856594d0f6a0a34ec32745cedd41fe3386a43981b41 -static/media/U-mana-symbol.2bc593b4.svg,1643427073596,e0ec1f04e58f3d7e902bc38175d32ac8b7f9531dff3576c039afd5f0c37f9679 -static/media/UB-mana-symbol.5902970d.svg,1643427073593,65e4edd46e499189812e54de9b33acfc235ded38bef520d358cb2e5864e28716 -static/media/UR-mana-symbol.dff648d3.svg,1643427073591,0169ce3ebcd03141fede815f7ac58fe6a666fb2e28c21e6f717a8fcdf58f9872 -static/media/UP-mana-symbol.fdfa4430.svg,1643427073608,dd73048fc537d5d332f67bec6b22b4147b14263b3d4a8e8bff5c7ebd59c0e7c5 -static/media/W-mana-symbol.ff5118d5.svg,1643427073594,ce2b17bda52c597fe4f8b6d0f60bb22a706663802785d8f5a8a74406d028e7cc -static/media/WP-mana-symbol.0583e2d3.svg,1643427073609,1e724800a0789953c1d8478f7467ee9a63f85f733017222e7012bfe2c1c71f71 -static/media/WB-mana-symbol.ed4fc3c0.svg,1643427073597,7ba7df73760063741441d59e46fff6161fdeb9ceac48bde019a0f6d1591c6d14 -static/media/X-mana-symbol.d689c2da.svg,1643427073609,a4b306eb869b8362987a3df54bbe01cb04c26f4a4ed04958c1d924a6bb9f89c9 -static/media/WU-mana-symbol.34afd6a0.svg,1643427073598,196944fc3e5e4df52f30fc66e9daf530e0ebdba63930de58f05244905a9cdecc -static/media/Y-mana-symbol.f286dfd2.svg,1643427073610,c32d0a5b890620d472e57e69f10d3b76e4135564b144bd9a561fcd3676236133 -static/media/Z-mana-symbol.e6cddc68.svg,1643427073612,83da18e6a254fe5d5ea91ad9d2881fbc1ce932654ceaefb83158ad4de5e5b0b6 -static/js/9.73ae37e1.chunk.js.map,1643427073635,367af038b3f2c5df5d0e253695bf0254c06e41cc6b707f45f2d89b5d9806c85d -static/js/6.69308370.chunk.js,1643427073618,99090d3b38e069142293249a594b9f096e9f33486fb5e4f181fdb50bad3a872b -static/js/8.21183cbc.chunk.js.map,1643427073635,92ac32ea08c51c00f204161cd895723a10c9b9c0dc98e8f2257d757fc33b0051 -static/js/7.acd1f312.chunk.js.map,1643427073634,21aef22e9c6af63f8b013ab2cb921acf60179b2255427fe6cfd0a48a82f918c6 -static/js/5.1086426c.chunk.js,1643427073617,1c4384f5b8d059d7cc35d26495395470bf069ce4986b05c1a52a36d9f5d8d000 -static/js/6.69308370.chunk.js.map,1643427073633,4a72d798e2dcb469a642359a1f5ed8b628900d2403097cf74d3473821ecc194a -static/js/5.1086426c.chunk.js.map,1643427073633,bfd43bca4597e48b36e4bfa4de6e201ce5dbe1e5cc9db92f74bfdf062ea5983c +asset-manifest.json,1644039869415,5e117104b5481920b338518de61530c49e733ccc2eccebbff7a3002ebea03399 +index.html,1644039869415,01c3966061eeb15d60ff4f4198fb329d9d53045350e33656a37ce93dfb8e4b05 +static/js/0.76925efd.chunk.js,1644039869383,816d59ec61193834d1f7e1dbf3e50eae9879a4175ad7b17050b8407f688ae66d +static/js/1.938d0699.chunk.js,1644039869384,5e5b5aa5550701b5f10ade70161e88cb837b8e1aaabc25a516275915c837cf4e +static/js/10.8b862c7a.chunk.js,1644039869391,0433571a7de1666d24b6267a0ddf70334a743f6c69e5f10609c452273c877612 +static/js/11.8b60ce12.chunk.js,1644039869391,634f6904f06898edcb22fea3acc55ff284923dc988ecd4710caf4986bfb3a0b1 +static/js/12.30e4383d.chunk.js,1644039869391,a9f4c3ddee03b5f197bf90655194380db8a4c49413e937f0f01e00a0db89b7b8 +static/js/0.76925efd.chunk.js.map,1644039869410,48f2eedfb9524c53a6f9ab7424af917fcbc289fd6ec2255ab1c56362fca356c6 +static/js/11.8b60ce12.chunk.js.map,1644039869414,c0ae13632f9f3dccc3eea7f41998c009a9bbe427392a74ab7854170a1fd7e442 +static/js/12.30e4383d.chunk.js.map,1644039869416,0be93a863e40e511b22bbf6d1cd96f8a2cdb1fec3e02d4ec0bb2b932b7229a93 +static/js/10.8b862c7a.chunk.js.map,1644039869413,e9e44f227fbd644990589a75d030d364f79000838cf1a4061fd6a3344765323e +static/js/14.4afa2c37.chunk.js,1644039869392,47c482b58cea969d5fa053e9f3b660bcf9b043264e89afd259941dafa881a8e1 +static/js/15.637f008f.chunk.js,1644039869392,ee4c7486b0afa076ebc7e5c8a0d3e955a98da0770c62e0e9429806eaa37128e0 +static/js/13.ef38124e.chunk.js,1644039869392,f52c3e389a25dcbcf7b7b5e8ada9f3a77a0f50d92cd4d229c8e230d066ef2648 +static/js/15.637f008f.chunk.js.map,1644039869417,6335ea044211a259911bd93466bc55564d184aa5aefe6fafcfb683b3820f287b +static/js/16.db9216b8.chunk.js,1644039869392,e083850aed4faabb93d515c73217ae4b61d89cd16eabe389b25d4bef106be9af +static/js/17.583c93b1.chunk.js,1644039869393,b98ed1adb3814946b812fb8c6b7ba10b87a7d48c1b1743c5838aad7267a43713 +static/js/17.583c93b1.chunk.js.map,1644039869417,29cda9f1adf2a63a75d28af5db093bc7ca386716420745cea8935baf301d4424 +static/js/19.b433dee7.chunk.js,1644039869395,475c0b5d878e2fbd57300ba550655c1369026ffb995e0892acf7cddbd98a932a +static/js/19.b433dee7.chunk.js.map,1644039869417,88a29fadc6260b822474a9984fad22e04d6ca955ab7925ce43393724252aadf0 +static/js/18.af6b77e2.chunk.js,1644039869393,754f0ac532594b5fdfea077c1d4c407d3949d76485b9c892d2394cf30a3bfdf5 +static/js/18.af6b77e2.chunk.js.map,1644039869417,3c72098fac5bbef1071e71f7bf71e9500a5d7da9fe055dce67dce130e0dd8aa9 +static/js/16.db9216b8.chunk.js.map,1644039869417,6ef7dcf5c5aae5b3a20dc7e3bb6cc8dbf3861ca5a372f1dd237837759775c2ec +static/js/2.1348955b.chunk.js,1644039869384,347b8fd1a287d6586f2145a86c036126b94f5d3c87885927a14cfdc5197a465e +static/js/5.e74424f1.chunk.js.LICENSE.txt,1644039869409,efc545aa142660a7bc9f4755a386ff5377fa19b279ccf9006b03ebdff35ce7c8 +static/js/6.94e3a82d.chunk.js.LICENSE.txt,1644039869409,a2e7f497cf638d332c05a0a3902858b24ebf118dd2dd6415e42a976f3eb4eef3 +static/js/runtime-main.44f23b99.js,1644039869385,5a1400a2722b210bd9b11f943aa06c0f61b27e93ae59c2808cc4f57703009397 +static/js/main.812eb847.chunk.js,1644039869384,13de8cc9a7223fa69d12c53e231619336b75a9ebf6c192643579ba6d2a62d7ba +static/media/0-mana-symbol.7d5443ee.svg,1644039869373,4dd9883b97ceff8afac0b50c81abca34d4a08224eb8f8c748e0b6ffcab18cfb4 +static/media/1-mana-symbol.34b61e5d.svg,1644039869373,5adc6117dc24a2862f0bb07e58b7e1516809a05e4c395814fb68069930e0eae8 +static/media/10-mana-symbol.595ceb65.svg,1644039869373,921cbd64cd8607b2242df1490a7abd48132014ef64b46fa24527053339336b0a +static/media/11-mana-symbol.214c7717.svg,1644039869374,929f77690c6f6300c587e0c20a26636b432f699c9bccd72c700eb141ed2029b1 +static/js/runtime-main.44f23b99.js.map,1644039869410,abf7d454c6b1dd1dbf0d95c7dfb4675896847d926ed53584bad8a111a7ee780b +static/media/12-mana-symbol.e5cdbe96.svg,1644039869374,ea5b6768c895ab0244954f7c2adf7248f134a0edc05187294e048e6e97ba1b4f +static/media/13-mana-symbol.d75cb718.svg,1644039869374,70a924eccefde61ab088be9b1b3033c318560e4a38c0b4451cdb33d705248e99 +static/js/14.4afa2c37.chunk.js.map,1644039869417,a9a656ef0cb17443fe778026757082ddddf5f111c13137727cc9a9e5c3f96097 +static/js/1.938d0699.chunk.js.map,1644039869410,7545136bcd55c72dbd4ba37a7533b3dc5889d9f08946d39b29a69e80e66338c0 +static/js/main.812eb847.chunk.js.map,1644039869410,5dcf78c408072caae96aed278a30e2c5ee23e42505137731f7dff84948b465cb +static/media/14-mana-symbol.9f239954.svg,1644039869377,a9d5f7899f0da43c688efb446d77d81435ebbbdc115ea99a4d1b1c03a4a2bbf0 +static/media/15-mana-symbol.a4113def.svg,1644039869378,c837d5f28afc1f802b0212f1af40167ed1bbc600a741d793c046e37364174bda +static/media/16-mana-symbol.48c7fbf1.svg,1644039869378,e40534b9151717cde9ed428c74a13241134fa1c5f1a319a32b53d6487b9cde9c +static/media/2-mana-symbol.f61762ff.svg,1644039869373,bd849225c8e1da5fc431c84d12314d5b7992aae38006b512e6043c8f483d5ecc +static/media/2B-mana-symbol.376e4820.svg,1644039869378,6b2753f01b24080ec4a5cc1be5e43456c53ddeedeea196fa1f77793cd5872ea1 +static/media/2G-mana-symbol.0e214514.svg,1644039869379,31e6d569719ca61f321f438b4910fbfcabd7d984bc10afbaebc206dbf20ed7d4 +static/media/2R-mana-symbol.1647b1a2.svg,1644039869378,236b9486a41a607fa671bd520c23573b8302a2b963a3b5e0aa49073ca8b6f04e +static/media/2U-mana-symbol.b6b79ece.svg,1644039869378,0be114fea30996c3193163aa125975efaca3b66e3bcc726d8380de09fed37f47 +static/media/3-mana-symbol.7c58f071.svg,1644039869373,033eb7bf73981a534fa6ba6f93cae9a562cdf43f2b1be7b9e1ad980b34dd3449 +static/media/4-mana-symbol.a15041e1.svg,1644039869373,8776931df9149e659ced87bfdf49bec32ad146fb21ca06f152f53dd7ef5b8a2e +static/media/2W-mana-symbol.93a636f4.svg,1644039869378,a803ae613eee5def3d5bf151dba6a86b726947ad96cba14b6514f2ed860d24ac +static/media/5-mana-symbol.4284cca1.svg,1644039869373,1bd40d8ead0bb386f4fce9fbc2d59669df68dd41cf9e51ae1aa04f3d7af2f226 +static/media/6-mana-symbol.49f9776a.svg,1644039869373,d896ea6aa09011b9c19b7568510c4a4103f03695a195622ddd9b8d1403caccbb +static/media/7-mana-symbol.42118de3.svg,1644039869373,5a67299b7be04a0fce1831b1c403a4c9e6e9ad2266a36306a5a468812d553c37 +static/media/8-mana-symbol.763a5d17.svg,1644039869373,5108532c7e0cce5d9ca45247ecf82e37fc6cb91bda8fb8080f4c2684e13d25d2 +static/media/9-mana-symbol.eaf18626.svg,1644039869373,302ba7a7b72f007d61ca1570b11edd83f4fb7a05071a4df5fd6fcb5dd0671aad +static/media/B-mana-symbol.39b24312.svg,1644039869367,d4fa39430c5ab736cbbcd44383f6dabe738050f6288f13f561cc6312daff9c2f +static/media/BP-mana-symbol.ebe58931.svg,1644039869379,5acdd1f8d51c1eab596b8451b413bcebf5beb17240feaf6023cb3893ad3afb63 +static/media/BG-mana-symbol.52cfa11d.svg,1644039869368,00563109665daa76b60e53dc53bd3bcca3a44025497be673ae1d67c76a854dbb +static/media/C-mana-symbol.52b07652.svg,1644039869368,836f38b29a8c3a63c63a95d3456857d4bd97921afdc4efecb9eadcf967df9cdc +static/media/BR-mana-symbol.2bceeba1.svg,1644039869368,daf8b9510464dfc513d105e5308567d8ade3655a6dff8d7decd23e69ce3a5a9c +static/media/G-mana-symbol.e3b3fa06.svg,1644039869367,0de0a219ac7d78244c498577c2aff10d6b74fb11376781fcb12e9b7ba1937843 +static/media/GP-mana-symbol.608640d1.svg,1644039869379,cc8bad90dad93031d9e21a436e369f1f6d63ac12c37b964bf91f3dd6ac999979 +static/media/gs1.4e1196f3.svg,1644039869383,942b4616520b10ba660cd6bfbc3fe09b439cdf939d4dea4c2fe6546440cb9d3c +static/media/GU-mana-symbol.a55e2ec9.svg,1644039869368,ca7afa5f8fbd94b85c111be9d12ec5815b4c9b80a98feb4838372fae8c461eb6 +static/media/GW-mana-symbol.608fd370.svg,1644039869368,7f8c12de61b8b9de9b6d3d0bd03e1b6932bdff58b3a36588df0a10e8fd0b1cab +static/js/13.ef38124e.chunk.js.map,1644039869417,9683b94437364581051c3e3579498ef8aa791ae9f41424c76497faab9a39f9c5 +static/media/iko.7c0a7c62.svg,1644039869383,01d1d2659e0cd745e32adbee25fc0213b79940bc7af2fa46d8ab4c131c340743 +static/media/jmp.56233b78.svg,1644039869384,b4cdc40cbafad76ae12a48e242e56ab46cbbb4cc8691d8f2ed79f6808890a897 +static/media/magic.9aa2f9b0.svg,1644039869384,c10de79e6cf1b4d1f5172bdb145653bee7ead63e30316fc1394f70a16c834493 +static/media/mor.97fa323f.svg,1644039869383,c7cc2ed96e29fa40e8e62d7cc1c1576bb5a335786145edeb9b2578f891840ce5 +static/media/mrd.72c4d3e8.svg,1644039869383,e2e5ba6e63525cfbe4436b6ef663bdd5b9b3eb9cfbaba0053d7c65100e2ef21a +static/media/ori.99fce50b.svg,1644039869383,5c9a318a104ed15c8df38af6c064be0109b9fc26dc4d774d09298735b78b3588 +static/media/planeswalker.e1165b9b.svg,1644039869383,fec243b594f9a41ccac327bc39c06daaef67e3a9f2cd5c924f198efa653b9ff9 +static/media/R-mana-symbol.6938c172.svg,1644039869367,c7ef65e73667aa5cff2e5c952e8d96c9c0598e48a72537dc797546ad1665283f +static/media/RG-mana-symbol.3312e337.svg,1644039869368,24d24027f3bd5e5bf5032e4ca398f50d016e0321550246457fcbef9ad98ed5a9 +static/media/RP-mana-symbol.6c1f176d.svg,1644039869379,86be684d7d553ffe9f6f83fde3992eadd02eab8e8f3231357127ea8688ccab37 +static/js/9.5c86c064.chunk.js,1644039869391,95a5c2a869b4f25454a1a9285f685bcda0ec3cbe9e1e0465fe26cbee2f58dbc2 +static/js/2.1348955b.chunk.js.map,1644039869410,fbb3cff161f4d0855e5d93c25fdbcc9fa5aade6b99a16085e2cc67b8abbc1620 +static/media/RW-mana-symbol.0f7771b2.svg,1644039869368,79688503fb15e217724ccb0652b13d3ae0ae88f43d5a91193757a20ecff12015 +static/js/8.655cdfee.chunk.js,1644039869391,730e72bb61379452763f79d278668ed69139ea8b023b4b0bf0e22bff6f140d8d +static/js/7.81155393.chunk.js,1644039869391,8ddb03c4238e5c7b33031a83c1c8c4ff879a6286bb9a87acb77a20bcc9769321 +static/media/tsr.6bf3706f.svg,1644039869383,e9ff1c3b2971a4589a5c7856594d0f6a0a34ec32745cedd41fe3386a43981b41 +static/media/U-mana-symbol.2bc593b4.svg,1644039869367,e0ec1f04e58f3d7e902bc38175d32ac8b7f9531dff3576c039afd5f0c37f9679 +static/media/UB-mana-symbol.5902970d.svg,1644039869368,65e4edd46e499189812e54de9b33acfc235ded38bef520d358cb2e5864e28716 +static/media/UP-mana-symbol.fdfa4430.svg,1644039869379,dd73048fc537d5d332f67bec6b22b4147b14263b3d4a8e8bff5c7ebd59c0e7c5 +static/media/UR-mana-symbol.dff648d3.svg,1644039869368,0169ce3ebcd03141fede815f7ac58fe6a666fb2e28c21e6f717a8fcdf58f9872 +static/media/W-mana-symbol.ff5118d5.svg,1644039869364,ce2b17bda52c597fe4f8b6d0f60bb22a706663802785d8f5a8a74406d028e7cc +static/media/WP-mana-symbol.0583e2d3.svg,1644039869379,1e724800a0789953c1d8478f7467ee9a63f85f733017222e7012bfe2c1c71f71 +static/media/WB-mana-symbol.ed4fc3c0.svg,1644039869369,7ba7df73760063741441d59e46fff6161fdeb9ceac48bde019a0f6d1591c6d14 +static/media/X-mana-symbol.d689c2da.svg,1644039869379,a4b306eb869b8362987a3df54bbe01cb04c26f4a4ed04958c1d924a6bb9f89c9 +static/media/Y-mana-symbol.f286dfd2.svg,1644039869381,c32d0a5b890620d472e57e69f10d3b76e4135564b144bd9a561fcd3676236133 +static/media/WU-mana-symbol.34afd6a0.svg,1644039869372,196944fc3e5e4df52f30fc66e9daf530e0ebdba63930de58f05244905a9cdecc +static/media/Z-mana-symbol.e6cddc68.svg,1644039869383,83da18e6a254fe5d5ea91ad9d2881fbc1ce932654ceaefb83158ad4de5e5b0b6 +static/js/9.5c86c064.chunk.js.map,1644039869413,5c20174f4ed9d9cb186f2ab81c467a7bc82e7cf78582b4b43aeca74c3ad0604b +static/js/6.94e3a82d.chunk.js,1644039869391,b2f14e9af1f2092608ce0fccf31c7889a374710dabc69db2c1a29f67608dcb15 +static/js/8.655cdfee.chunk.js.map,1644039869413,a707a0b66e95cceeb4de0c8befa937afaf3aecb36debabc1a7f1301dbb5ac4e6 +static/js/7.81155393.chunk.js.map,1644039869412,8441903d3b49f74630095c3394bf095c9b6e6121032135ba7278639a38090481 +static/js/5.e74424f1.chunk.js,1644039869391,3ee71a1556fde2f30f90d556e0fd08809ef873583c2f2fb224892353623196cf +static/js/6.94e3a82d.chunk.js.map,1644039869412,814f647bfdf99c39e2edd78c2ebdfc578710beb47f033211dd43885a6d3f2f06 +static/js/5.e74424f1.chunk.js.map,1644039869412,e4579d65ca97630a410534f0d86f600cd82c300489cf794c172a529675c1e44d diff --git a/.prettierrc b/.prettierrc index c7ba1ef..d49d385 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,7 +5,7 @@ "endOfLine": "lf", "htmlWhitespaceSensitivity": "css", "jsxSingleQuote": false, - "printWidth": 80, + "printWidth": 100, "proseWrap": "preserve", "quoteProps": "as-needed", "rangeStart": 0, diff --git a/public/index.html b/public/index.html index 5de9851..544aeea 100644 --- a/public/index.html +++ b/public/index.html @@ -4,7 +4,7 @@ - + - client.url === notification.data.url && - client.visibilityState === 'hidden' + (client) => client.url === notification.data.url && client.visibilityState === 'hidden' ); if (hiddenApp) { @@ -64,7 +62,7 @@ self.addEventListener('push', async function (event) { data: { url: data.url }, - icon: '/images/icon.png' + icon: data.icon ?? '/images/icon.png' }); } } diff --git a/src/components/Account Page/BudAccordion.jsx b/src/components/Account Page/BudAccordion.jsx index b7a2fa4..81f5907 100644 --- a/src/components/Account Page/BudAccordion.jsx +++ b/src/components/Account Page/BudAccordion.jsx @@ -15,6 +15,9 @@ import MUITypography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; import customSort from '../../functions/custom-sort'; +import initiateBudRequest from '../../graphql/mutations/account/initiate-bud-request'; +import respondToBudRequest from '../../graphql/mutations/account/respond-to-bud-request'; +import revokeBudship from '../../graphql/mutations/account/revoke-budship'; import Avatar from '../miscellaneous/Avatar'; import ConfirmationDialog from '../miscellaneous/ConfirmationDialog'; import { AccountContext } from '../../contexts/account-context'; @@ -48,20 +51,10 @@ export default function BudAccordion() { const { accountID } = useParams(); const classes = useStyles(); const { - accountState: { - buds, - nearby_users, - received_bud_requests, - sent_bud_requests - }, - editAccount + accountState: { buds, nearby_users, received_bud_requests, sent_bud_requests } } = useContext(AccountContext); const { geolocationEnabled, userID } = useContext(AuthenticationContext); - const [budToDelete, setBudToDelete] = useState({ - _id: null, - avatar: null, - name: null - }); + const [budToDelete, setBudToDelete] = useState(); const [recommendedBuds, setRecommendedBuds] = useState([]); useEffect(() => { @@ -101,25 +94,19 @@ export default function BudAccordion() { { - editAccount(`action: "remove",\nother_user_id: "${budToDelete._id}"`); - setBudToDelete({ _id: null, avatar: null, name: null }); + revokeBudship({ + variables: { other_user_id: budToDelete ? budToDelete._id : '' } + }); + setBudToDelete(null); }} - open={!!budToDelete._id} - title={`Are you sure you want to un-bud ${budToDelete.name}?`} - toggleOpen={() => - setBudToDelete({ _id: null, avatar: null, name: null }) - } + open={!!budToDelete} + title={`Are you sure you want to un-bud ${budToDelete ? budToDelete.name : ''}?`} + toggleOpen={() => setBudToDelete(null)} >
- + - Think of all the good times you've had. And how lonely they'll be - without you. + Think of all the good times you've had. And how lonely they'll be without you.
@@ -149,9 +136,7 @@ export default function BudAccordion() { horizontal: 'right', vertical: 'top' }} - badgeContent={ - - } + badgeContent={} className={classes.badge} color="primary" onClick={(event) => { @@ -160,28 +145,22 @@ export default function BudAccordion() { .closest('span') .classList.contains('MuiBadge-colorPrimary') ) { - editAccount( - `action: "send",\nother_user_id: "${nearby_user._id}"` - ); + initiateBudRequest({ + variables: { other_user_id: nearby_user._id } + }); } }} overlap="circular" > - + ))} ) : ( - - Determining Location... - + Determining Location... )}
)} @@ -198,9 +177,7 @@ export default function BudAccordion() { horizontal: 'right', vertical: 'bottom' }} - badgeContent={ - - } + badgeContent={} className={classes.badge} color="secondary" onClick={(event) => { @@ -209,9 +186,9 @@ export default function BudAccordion() { .closest('span') .classList.contains('MuiBadge-colorSecondary') ) { - editAccount( - `action: "reject",\nother_user_id: "${request._id}"` - ); + respondToBudRequest({ + variables: { other_user_id: request._id, response: 'reject' } + }); } }} overlap="circular" @@ -221,9 +198,7 @@ export default function BudAccordion() { horizontal: 'right', vertical: 'top' }} - badgeContent={ - - } + badgeContent={} className={classes.badge} color="primary" onClick={(event) => { @@ -232,19 +207,15 @@ export default function BudAccordion() { .closest('span') .classList.contains('MuiBadge-colorPrimary') ) { - editAccount( - `action: "accept",\nother_user_id: "${request._id}"` - ); + respondToBudRequest({ + variables: { other_user_id: request._id, response: 'accept' } + }); } }} overlap="circular" > - + @@ -261,11 +232,7 @@ export default function BudAccordion() { return ( - + ); @@ -281,26 +248,22 @@ export default function BudAccordion() { - } + badgeContent={} className={classes.badge} color="primary" onClick={(event) => { if ( - event.target - .closest('span') - .classList.contains('MuiBadge-colorPrimary') + event.target.closest('span').classList.contains('MuiBadge-colorPrimary') ) { - editAccount( - `action: "send",\nother_user_id: "${pb._id}"` - ); + initiateBudRequest({ + variables: { other_user_id: pb._id } + }); } }} overlap="circular" > - + @@ -319,16 +282,12 @@ export default function BudAccordion() { {accountID === userID ? ( - } + badgeContent={} className={classes.badge} color="secondary" onClick={(event) => { if ( - event.target - .closest('span') - .classList.contains('MuiBadge-colorSecondary') + event.target.closest('span').classList.contains('MuiBadge-colorSecondary') ) { setBudToDelete(bud); } @@ -336,12 +295,12 @@ export default function BudAccordion() { overlap="circular" > - + ) : ( - + )} diff --git a/src/components/Account Page/CubeAccordion.jsx b/src/components/Account Page/CubeAccordion.jsx index f2a2177..9ce782f 100644 --- a/src/components/Account Page/CubeAccordion.jsx +++ b/src/components/Account Page/CubeAccordion.jsx @@ -36,10 +36,7 @@ export default function CubeAccordion({ pageClasses }) { return ( - + {cube.image && ( {cube.image.alt} )} @@ -93,9 +98,7 @@ export default function CubeAccordion({ pageClasses }) { - setCubeToDelete({ _id: cube._id, name: cube.name }) - } + onClick={() => setCubeToDelete({ _id: cube._id, name: cube.name })} size="small" > diff --git a/src/components/Account Page/DeckAccordion.jsx b/src/components/Account Page/DeckAccordion.jsx index e1c7cf3..74c6827 100644 --- a/src/components/Account Page/DeckAccordion.jsx +++ b/src/components/Account Page/DeckAccordion.jsx @@ -40,10 +40,7 @@ export default function DeckAccordion({ pageClasses }) { toggleOpen={() => setShowCreateDeckForm((prevState) => !prevState)} /> - + {deck.image && ( {deck.image.alt} )} @@ -90,9 +95,7 @@ export default function DeckAccordion({ pageClasses }) { - setDeckToDelete({ _id: deck._id, name: deck.name }) - } + onClick={() => setDeckToDelete({ _id: deck._id, name: deck.name })} size="small" > diff --git a/src/components/Account Page/EventAccordion.jsx b/src/components/Account Page/EventAccordion.jsx index ab70956..9e77c8c 100644 --- a/src/components/Account Page/EventAccordion.jsx +++ b/src/components/Account Page/EventAccordion.jsx @@ -34,8 +34,7 @@ export default function EventAccordion() { const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(5); const [showEventForm, setShowEventForm] = useState(false); - const emptyRows = - page > 0 ? Math.max(0, (1 + page) * rowsPerPage - total_events) : 0; + const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - total_events) : 0; return ( @@ -60,10 +59,7 @@ export default function EventAccordion() { event.players.length)) * 50 - }px` + minWidth: `${400 + Math.max(...events.map((event) => event.players.length)) * 50}px` }} > @@ -78,10 +74,7 @@ export default function EventAccordion() { {(rowsPerPage > 0 && total_events > 5 - ? events.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ) + ? events.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) : events ).map(function (event) { return ( @@ -99,9 +92,17 @@ export default function EventAccordion() { > {event.cube.image && ( {event.cube.image.alt} )} @@ -109,11 +110,7 @@ export default function EventAccordion() { - + @@ -125,10 +122,9 @@ export default function EventAccordion() { ) .map((player) => ( ))} @@ -149,12 +145,7 @@ export default function EventAccordion() { {total_events > 5 && ( {matches.map(function (match) { - const opponent = match.players.find( - (player) => player.account._id !== accountID - ); + const opponent = match.players.find((player) => player.account._id !== accountID); return ( - + {match.decks && match.decks.map((deck) => ( @@ -87,9 +82,7 @@ export default function MatchAccordion({ pageClasses }) { {match.event && ( - - {match.event.name} - + {match.event.name} )} @@ -105,9 +98,7 @@ export default function MatchAccordion({ pageClasses }) { {accountID === userID && ( - setShowMatchForm(true)}> - Create a Match - + setShowMatchForm(true)}>Create a Match )} diff --git a/src/components/Cube Page/CloneCubeButton.jsx b/src/components/Cube Page/CloneCubeButton.jsx new file mode 100644 index 0000000..aa7dd94 --- /dev/null +++ b/src/components/Cube Page/CloneCubeButton.jsx @@ -0,0 +1,57 @@ +import React, { useContext, useState } from 'react'; +import MUIButton from '@mui/material/Button'; +import MUICircularProgress from '@mui/material/CircularProgress'; +import MUICloudDoneOutlinedIcon from '@mui/icons-material/CloudDoneOutlined'; +import MUIFileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined'; +import { useNavigate } from 'react-router-dom'; + +import cloneCube from '../../graphql/mutations/cube/clone-cube'; +import cubeQuery from '../../constants/cube-query'; +import { ErrorContext } from '../../contexts/Error'; + +export default function CloneCubeButton({ CubeID }) { + const { setErrorMessages } = useContext(ErrorContext); + const navigate = useNavigate(); + const [cloning, setCloning] = useState(false); + const [success, setSuccess] = useState(false); + + return ( + { + if (!success) { + try { + setCloning(true); + const data = await cloneCube({ + headers: { CubeID }, + queryString: cubeQuery + }); + setSuccess(true); + setTimeout(() => { + navigate(`/cube/${data.data.cloneCube._id}`, { + state: { cubeData: data.data.cloneCube } + }); + setSuccess(false); + }, 1000); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setCloning(false); + } + } + }} + startIcon={(() => { + if (cloning) { + return ; + } + if (success) { + return ; + } + return ; + })()} + > + Clone Cube + + ); +} diff --git a/src/components/Cube Page/CubeDashboard.jsx b/src/components/Cube Page/CubeDashboard.jsx index 4b2ed47..cc79561 100644 --- a/src/components/Cube Page/CubeDashboard.jsx +++ b/src/components/Cube Page/CubeDashboard.jsx @@ -5,39 +5,41 @@ import MUICard from '@mui/material/Card'; import MUICardActions from '@mui/material/CardActions'; import MUICardContent from '@mui/material/CardContent'; import MUICardHeader from '@mui/material/CardHeader'; -import MUICheckbox from '@mui/material/Checkbox'; import MUIDeleteForeverOutlinedIcon from '@mui/icons-material/DeleteForeverOutlined'; import MUIDialog from '@mui/material/Dialog'; import MUIDialogActions from '@mui/material/DialogActions'; import MUIDialogContent from '@mui/material/DialogContent'; import MUIDialogTitle from '@mui/material/DialogTitle'; import MUIEditOutlinedIcon from '@mui/icons-material/EditOutlined'; -import MUIFileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined'; import MUIFormControl from '@mui/material/FormControl'; -import MUIFormControlLabel from '@mui/material/FormControlLabel'; -import MUIHelpOutlineIcon from '@mui/icons-material/HelpOutline'; import MUIIconButton from '@mui/material/IconButton'; import MUIImageList from '@mui/material/ImageList'; import MUIImageListItem from '@mui/material/ImageListItem'; -import MUIInputAdornment from '@mui/material/InputAdornment'; import MUIInputLabel from '@mui/material/InputLabel'; import MUISelect from '@mui/material/Select'; import MUIShuffleOutlinedIcon from '@mui/icons-material/ShuffleOutlined'; import MUITextField from '@mui/material/TextField'; -import MUITooltip from '@mui/material/Tooltip'; import MUITypography from '@mui/material/Typography'; import useMediaQuery from '@mui/material/useMediaQuery'; import { CSVLink } from 'react-csv'; import { Link } from 'react-router-dom'; +import CloneCubeButton from './CloneCubeButton'; +import CubeDescriptionInput from './CubeDescriptionInput'; +import CubeFilterInput from './CubeFilterInput'; +import CubeNameInput from './CubeNameInput'; +import CubePublishedCheckbox from './CubePublishedCheckbox'; import CreateComponentForm from './CreateComponentForm'; import DeleteCubeForm from '../../forms/DeleteCubeForm'; import ScryfallRequest from '../miscellaneous/ScryfallRequest'; +import editCube from '../../graphql/mutations/cube/edit-cube'; +import editModule from '../../graphql/mutations/cube/edit-module'; import generateCSVList from '../../functions/generate-csv-list'; import randomSampleWOReplacement from '../../functions/random-sample-wo-replacement'; import theme from '../../theme'; import { AuthenticationContext } from '../../contexts/Authentication'; import { CubeContext } from '../../contexts/cube-context'; +import { ErrorContext } from '../../contexts/Error'; export default function CubeDashboard() { const { isLoggedIn, userID } = useContext(AuthenticationContext); @@ -51,42 +53,27 @@ export default function CubeDashboard() { mainboard, modules, name: cubeName, - published, rotations, sideboard }, - cloneCube, deleteModule, deleteRotation, - displayState, - editCube, - editModule, editRotation, setDisplayState } = useContext(CubeContext); - const [createComponentDialogIsOpen, setCreateComponentDialogIsOpen] = - useState(false); - const [cubeNameInput, setCubeNameInput] = useState(cubeName); + const { setErrorMessages } = useContext(ErrorContext); + const cubeImageWidth = useMediaQuery(theme.breakpoints.up('md')) ? 150 : 75; + const componentNameInputRef = useRef(); + const [componentNameInput, setComponentNameInput] = useState(activeComponentState.name); + const [createComponentDialogIsOpen, setCreateComponentDialogIsOpen] = useState(false); const [cubeToDelete, setCubeToDelete] = useState({ _id: null, name: null }); - const [descriptionInput, setDescriptionInput] = useState(description); const [editingComponentName, setEditingComponentName] = useState(false); - const [isPublished, setIsPublished] = useState(published); const [samplePack, setSamplePack] = useState([]); const [sizeInput, setSizeInput] = useState(activeComponentState.size); - const componentNameInputRef = useRef(); - const cubeImageWidth = useMediaQuery(theme.breakpoints.up('md')) ? 150 : 75; - - useEffect(() => { - setCubeNameInput(cubeName); - }, [cubeName]); - - useEffect(() => { - setDescriptionInput(description); - }, [description]); useEffect(() => { - setIsPublished(published); - }, [published]); + setComponentNameInput(activeComponentState.name); + }, [activeComponentState.name]); useEffect(() => { setSizeInput(activeComponentState.size); @@ -100,21 +87,13 @@ export default function CubeDashboard() { {samplePack.map((card) => ( - {card.name} + {card.name} ))} - - setSamplePack(randomSampleWOReplacement(mainboard, 15)) - } - > + setSamplePack(randomSampleWOReplacement(mainboard, 15))}> New Sample Pack @@ -122,15 +101,10 @@ export default function CubeDashboard() { - setCreateComponentDialogIsOpen((prevState) => !prevState) - } + toggleOpen={() => setCreateComponentDialogIsOpen((prevState) => !prevState)} /> - + {!editingComponentName && (
- {!['mainboard', 'sideboard'].includes( - activeComponentState._id - ) && + {!['mainboard', 'sideboard'].includes(activeComponentState._id) && userID === creator._id && ( { setEditingComponentName(true); - setTimeout( - () => componentNameInputRef.current.focus(), - 0 - ); + setTimeout(() => componentNameInputRef.current.focus(), 0); }} > @@ -158,9 +127,7 @@ export default function CubeDashboard() { )} - - Viewing - + Viewing { - if ( - activeComponentState.name !== - componentNameInputRef.current.value - ) { + onBlur: async () => { + setEditingComponentName(false); + if (activeComponentState.name !== componentNameInputRef.current.value) { if (Number.isInteger(activeComponentState.size)) { - editRotation( - componentNameInputRef.current.value, - sizeInput - ); + editRotation(componentNameInputRef.current.value, sizeInput); } else { - editModule(componentNameInputRef.current.value); + try { + await editModule({ + headers: { CubeID: cubeID }, + queryString: `{\n_id\n}`, + variables: { + moduleID: activeComponentState._id, + name: componentNameInput + } + }); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + setComponentNameInput(activeComponentState.name); + } } } - setEditingComponentName(false); } }} inputRef={componentNameInputRef} @@ -237,8 +208,7 @@ export default function CubeDashboard() { inputProps={{ max: activeComponentState.maxSize, min: 0, - onBlur: () => - editRotation(activeComponentState.name, sizeInput), + onBlur: () => editRotation(activeComponentState.name, sizeInput), step: 1 }} onChange={(event) => setSizeInput(event.target.value)} @@ -246,9 +216,7 @@ export default function CubeDashboard() { marginLeft: !editingComponentName && userID === creator._id && - !['mainboard', 'sideboard'].includes( - activeComponentState._id - ) + !['mainboard', 'sideboard'].includes(activeComponentState._id) ? 40 : 0, marginTop: 8 @@ -262,69 +230,31 @@ export default function CubeDashboard() { avatar={ image && ( {image.alt} ) } title={ - - { - editCube( - descriptionInput, - image.scryfall_id, - cubeNameInput, - isPublished - ); - } - }} - label="Cube Name" - onChange={(event) => setCubeNameInput(event.target.value)} - type="text" - value={cubeNameInput} - /> - {creator._id === userID && ( -
- { - editCube( - descriptionInput, - image.scryfall_id, - cubeNameInput, - !isPublished - ); - setIsPublished((prevState) => !prevState); - }} - /> - } - label="Published" - style={{ marginRight: 8 }} - /> - - - -
- )} -
+ creator._id === userID ? ( + + + + + ) : ( + {cubeName} + ) } subheader={ - Designed by:{' '} - {creator.name} + Designed by: {creator.name} module.cards) .flat() - .concat( - rotations.map((rotation) => rotation.cards).flat() - ) + .concat(rotations.map((rotation) => rotation.cards).flat()) .concat(sideboard) )} filename={`${cubeName}.csv`} @@ -349,69 +277,31 @@ export default function CubeDashboard() { /> - { - editCube( - descriptionInput, - image.scryfall_id, - cubeNameInput, - isPublished - ); - } - }} - label="Cube Description" - multiline - onChange={(event) => setDescriptionInput(event.target.value)} - rows={2} - style={{ marginBottom: 8 }} - value={descriptionInput} - /> + {creator._id === userID ? ( + + ) : ( + {description} + )} {userID === creator._id && ( { - editCube( - descriptionInput, - chosenCard.scryfall_id, - cubeNameInput, - isPublished - ); + onSubmit={async (chosenCard) => { + try { + await editCube({ + headers: { CubeID: cubeID }, + queryString: `{\n_id\nimage\n}`, + variables: { image: chosenCard._id } + }); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } }} /> )} - - - Matches:{' '} - - {activeComponentState.displayedCards.length} - - - - ) - }} - label="Filter by keywords, name or type" - margin="normal" - onChange={(event) => { - event.persist(); - setDisplayState((prevState) => ({ - ...prevState, - filter: event.target.value - })); - }} - type="text" - value={displayState.filter} - /> + - {!['mainboard', 'sideboard'].includes( - activeComponentState._id - ) && ( + {!['mainboard', 'sideboard'].includes(activeComponentState._id) && ( } > - Delete this{' '} - {Number.isInteger(activeComponentState.size) - ? 'Rotation' - : 'Module'} + Delete this {Number.isInteger(activeComponentState.size) ? 'Rotation' : 'Module'} )} )} - {isLoggedIn && ( - } - > - Clone Cube - - )} + {isLoggedIn && } {userID === creator._id && ( - setSamplePack(randomSampleWOReplacement(mainboard, 15)) - } + onClick={() => setSamplePack(randomSampleWOReplacement(mainboard, 15))} startIcon={} > Sample Pack diff --git a/src/components/Cube Page/CubeDescriptionInput.jsx b/src/components/Cube Page/CubeDescriptionInput.jsx new file mode 100644 index 0000000..39d7f1d --- /dev/null +++ b/src/components/Cube Page/CubeDescriptionInput.jsx @@ -0,0 +1,47 @@ +import React, { useContext, useEffect, useRef, useState } from 'react'; +import MUITextField from '@mui/material/TextField'; + +import editCube from '../../graphql/mutations/cube/edit-cube'; +import { CubeContext } from '../../contexts/cube-context'; +import { ErrorContext } from '../../contexts/Error'; + +export default function CubeDescriptionInput() { + const { + cubeState: { _id: cubeID, description } + } = useContext(CubeContext); + const { setErrorMessages } = useContext(ErrorContext); + const cubeDescriptionInputRef = useRef(); + const [cubeDescriptionInputState, setCubeDescriptionInputState] = + useState(description); + + useEffect(() => { + setCubeDescriptionInputState(description); + }, [description]); + + return ( + { + try { + const data = await editCube({ + headers: { CubeID: cubeID }, + queryString: `{\n_id\ndescription\n}`, + variables: { description: cubeDescriptionInputState } + }); + setCubeDescriptionInputState(data.data.editCube.description); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } + } + }} + label="Cube Description" + margin="normal" + multiline + onChange={(event) => setCubeDescriptionInputState(event.target.value)} + ref={cubeDescriptionInputRef} + rows={2} + value={cubeDescriptionInputState} + /> + ); +} diff --git a/src/components/Cube Page/CubeDisplay.jsx b/src/components/Cube Page/CubeDisplay.jsx index 7d4a35d..7305acb 100644 --- a/src/components/Cube Page/CubeDisplay.jsx +++ b/src/components/Cube Page/CubeDisplay.jsx @@ -13,10 +13,7 @@ import specificCardType from '../../functions/specific-card-type'; import theme from '../../theme'; import HoverPreview from '../miscellaneous/HoverPreview'; import { monoColors, multiColors } from '../../constants/color-objects'; -import { - generalCardTypes, - specificCardTypes -} from '../../constants/type-objects'; +import { generalCardTypes, specificCardTypes } from '../../constants/type-objects'; import { CubeContext } from '../../contexts/cube-context'; import { ReactComponent as MagicSVG } from '../../svgs/magic.svg'; @@ -137,13 +134,15 @@ export default function CubeDisplay({ setSelectedCard }) {
{monoColors.map(function (color) { const cards_color = displayedCards.filter( - (card) => card.color_identity.toString() === color.color_identity + (card) => + (card.color_identity + ? card.color_identity + : card.scryfall_card.color_identity + ).toString() === color.color_identity ); return ( - ({cards_color.length}) - - } + title={({cards_color.length})} /> {specificCardTypes.map(function (type) { const cards_color_type = cards_color.filter( - (card) => specificCardType(card.type_line) === type.name + (card) => + specificCardType( + card.type_line ? card.type_line : card.scryfall_card.type_line + ) === type.name ); return ( {cards_color_type.length > 0 && ( - + {React.cloneElement(type.svg, { style: { height: 20, marginRight: 8, width: 20 } })} @@ -177,50 +172,58 @@ export default function CubeDisplay({ setSelectedCard }) {
{[...Array(16).keys()].map(function (cost) { - const cards_color_type_cost = - cards_color_type.filter( - (card) => card.cmc === cost - ); + const cards_color_type_cost = cards_color_type.filter( + (card) => + (Number.isInteger(card.cmc) ? card.cmc : card.scryfall_card.cmc) === + cost + ); return ( {cards_color_type_cost.length > 0 && (
- {customSort(cards_color_type_cost, [ - 'name' - ]).map(function (card, index) { - return ( - - - - setSelectedCard(card) + {customSort(cards_color_type_cost, ['scryfall_card.name']).map( + function (card, index) { + return ( + + - {index + 1}) {card.name} - {!card.mtgo_id && ( - - - - )} - - - - ); - })} + setSelectedCard(card)} + style={{ cursor: 'pointer' }} + variant="body1" + > + {index + 1}){' '} + {card.name ? card.name : card.scryfall_card.name} + {!card.scryfall_card.mtgo_id && ( + + + + )} + + + + ); + } + )}
)}
@@ -244,8 +247,9 @@ export default function CubeDisplay({ setSelectedCard }) { ( { - displayedCards.filter((card) => card.color_identity.length > 1) - .length + displayedCards.filter( + (card) => (card.color_identity ?? card.scryfall_card.color_identity).length > 1 + ).length } ) @@ -254,20 +258,17 @@ export default function CubeDisplay({ setSelectedCard }) { {multiColors.map(function (color) { const cards_color = displayedCards.filter( - (card) => card.color_identity.toString() === color.color_identity + (card) => + (card.color_identity ?? card.scryfall_card.color_identity).toString() === + color.color_identity ); return ( {cards_color.length > 0 && (
- + {color.svg && React.cloneElement(color.svg, { style: { height: 24, marginRight: 8, width: 24 } @@ -276,7 +277,9 @@ export default function CubeDisplay({ setSelectedCard }) { {generalCardTypes.map(function (type) { const cards_color_type = cards_color.filter( - (card) => type.name === generalCardType(card.type_line) + (card) => + generalCardType(card.type_line ?? card.scryfall_card.type_line) === + type.name ); return ( cards_color_type.length > 0 && ( @@ -293,44 +296,51 @@ export default function CubeDisplay({ setSelectedCard }) { })} {type.name} - {customSort(cards_color_type, ['cmc']).map( - function (card, index) { - return ( - - + + setSelectedCard(card)} + style={{ + cursor: 'pointer', + userSelect: 'none' + }} + variant="body1" > - - setSelectedCard(card) - } - style={{ - cursor: 'pointer', - userSelect: 'none' - }} - variant="body1" - > - {index + 1}) {card.name} - {!card.mtgo_id && ( - - - - )} - - - - ); - } - )} + {index + 1}) {card.name ?? card.scryfall_card.name} + {!card.scryfall_card.mtgo_id && ( + + + + )} + + + + ); + })}
) ); diff --git a/src/components/Cube Page/CubeFilterInput.jsx b/src/components/Cube Page/CubeFilterInput.jsx new file mode 100644 index 0000000..0928ab3 --- /dev/null +++ b/src/components/Cube Page/CubeFilterInput.jsx @@ -0,0 +1,47 @@ +import React, { useContext, useRef, useState } from 'react'; +import MUIInputAdornment from '@mui/material/InputAdornment'; +import MUITextField from '@mui/material/TextField'; + +import { CubeContext } from '../../contexts/cube-context'; + +export default function CubeFilterInput() { + const { + activeComponentState, + displayState: { filter }, + setDisplayState + } = useContext(CubeContext); + const timer = useRef(); + const [cubeFilterInputState, setCubeFilterInputState] = useState(filter); + + return ( + + + Matches:{' '} + {activeComponentState.displayedCards.length} + + + ) + }} + label="Filter by keywords, name or type" + margin="normal" + onChange={(event) => { + event.persist(); + clearTimeout(timer.current); + setCubeFilterInputState(event.target.value); + timer.current = setTimeout(() => { + setDisplayState((prevState) => ({ + ...prevState, + filter: event.target.value + })); + }, 100); + }} + type="text" + value={cubeFilterInputState} + /> + ); +} diff --git a/src/components/Cube Page/CubeNameInput.jsx b/src/components/Cube Page/CubeNameInput.jsx new file mode 100644 index 0000000..4c01b54 --- /dev/null +++ b/src/components/Cube Page/CubeNameInput.jsx @@ -0,0 +1,44 @@ +import React, { useContext, useEffect, useRef, useState } from 'react'; +import MUITextField from '@mui/material/TextField'; + +import editCube from '../../graphql/mutations/cube/edit-cube'; +import { CubeContext } from '../../contexts/cube-context'; +import { ErrorContext } from '../../contexts/Error'; + +export default function CubeNameInput() { + const { + cubeState: { _id: cubeID, name: cubeName } + } = useContext(CubeContext); + const { setErrorMessages } = useContext(ErrorContext); + const cubeNameInputRef = useRef(); + const [cubeNameInputState, setCubeNameInputState] = useState(cubeName); + + useEffect(() => { + setCubeNameInputState(cubeName); + }, [cubeName]); + + return ( + { + try { + const data = await editCube({ + headers: { CubeID: cubeID }, + queryString: `{\n_id\nname\n}`, + variables: { name: cubeNameInputState } + }); + setCubeNameInputState(data.data.editCube.name); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + setCubeNameInputState(cubeName); + } + } + }} + label="Cube Name" + onChange={(event) => setCubeNameInputState(event.target.value)} + ref={cubeNameInputRef} + type="text" + value={cubeNameInputState} + /> + ); +} diff --git a/src/components/Cube Page/CubePublishedCheckbox.jsx b/src/components/Cube Page/CubePublishedCheckbox.jsx new file mode 100644 index 0000000..9d500aa --- /dev/null +++ b/src/components/Cube Page/CubePublishedCheckbox.jsx @@ -0,0 +1,59 @@ +import React, { useContext, useEffect, useState } from 'react'; +import MUICheckbox from '@mui/material/Checkbox'; +import MUIFormControlLabel from '@mui/material/FormControlLabel'; +import MUIHelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import MUITooltip from '@mui/material/Tooltip'; + +import editCube from '../../graphql/mutations/cube/edit-cube'; +import { CubeContext } from '../../contexts/cube-context'; +import { ErrorContext } from '../../contexts/Error'; + +export default function CubePublishedCheckbox() { + const { + cubeState: { _id: cubeID, published } + } = useContext(CubeContext); + const { setErrorMessages } = useContext(ErrorContext); + const [publishedCheckedState, setPublishedCheckedState] = useState(published); + + useEffect(() => { + setPublishedCheckedState(published); + }, [published]); + + useEffect(() => { + (async function () { + try { + await editCube({ + headers: { CubeID: cubeID }, + queryString: `{\n_id\npublished\n}`, + variables: { published: publishedCheckedState } + }); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + setPublishedCheckedState(published); + } + })(); + }, [publishedCheckedState]); + + return ( +
+ setPublishedCheckedState((prevState) => !prevState)} + /> + } + label="Published" + style={{ marginRight: 8 }} + /> + + + +
+ ); +} diff --git a/src/components/Cube Page/EditCardModal.jsx b/src/components/Cube Page/EditCardModal.jsx index cc6295f..524436e 100644 --- a/src/components/Cube Page/EditCardModal.jsx +++ b/src/components/Cube Page/EditCardModal.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext, useState } from 'react'; import MUIButton from '@mui/material/Button'; import MUICancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import MUIDialog from '@mui/material/Dialog'; @@ -8,8 +8,10 @@ import MUIDialogTitle from '@mui/material/DialogTitle'; import MUIGrid from '@mui/material/Grid'; import MUIPublishedWithChangesOutlinedIcon from '@mui/icons-material/PublishedWithChangesOutlined'; import MUITextField from '@mui/material/TextField'; +import { useParams } from 'react-router-dom'; -import ChangePrintMenu from './ChangePrintMenu'; +import editCard from '../../graphql/mutations/cube/edit-card'; +// import ChangePrintMenu from './ChangePrintMenu'; import ColorCheckboxes from './ColorCheckboxes'; import MoveDeleteMenu from './MoveDeleteMenu'; import { CubeContext } from '../../contexts/cube-context'; @@ -17,69 +19,61 @@ import { CubeContext } from '../../contexts/cube-context'; export default function EditCardModal({ card, clear, editable }) { const { activeComponentState: { _id: activeComponentID }, - deleteCard, - editCard - } = React.useContext(CubeContext); - const [destination, setDestination] = React.useState(activeComponentID); - const [mutableCardDetails, setMutableCardDetails] = React.useState({ - cmc: card.cmc, - color_identity: card.color_identity, + deleteCard + } = useContext(CubeContext); + const { cubeID } = useParams(); + const [destination, setDestination] = useState(activeComponentID); + const [mutableCardDetails, setMutableCardDetails] = useState({ + cmc: Number.isInteger(card.cmc) ? card.cmc : card.scryfall_card.cmc, + color_identity: card.color_identity ? card.color_identity : card.scryfall_card.color_identity, notes: card.notes, - scryfall_id: card.scryfall_id, - type_line: card.type_line + scryfall_id: card.scryfall_card._id, + type_line: card.type_line ? card.type_line : card.scryfall_card.type_line }); - const submitForm = React.useCallback( - async (event) => { - event.preventDefault(); + async function submitForm(event) { + event.preventDefault(); - if ( - JSON.stringify(mutableCardDetails) !== - JSON.stringify({ - cmc: card.cmc, - color_identity: card.color_identity, - notes: card.notes, - scryfall_id: card.scryfall_id, - type_line: card.type_line - }) - ) { - await editCard( - `cardID: "${card._id}",\ncmc: ${ - mutableCardDetails.cmc - },\ncolor_identity: [${mutableCardDetails.color_identity.map( - (ci) => '"' + ci + '"' - )}],\nnotes: "${mutableCardDetails.notes}",\nscryfall_id: "${ - mutableCardDetails.scryfall_id - }",\ntype_line: "${mutableCardDetails.type_line}"` - ); - } + if ( + JSON.stringify(mutableCardDetails) !== + JSON.stringify({ + cmc: Number.isInteger(card.cmc) ? card.cmc : card.scryfall_card.cmc, + color_identity: card.color_identity + ? card.color_identity + : card.scryfall_card.color_identity, + notes: card.notes, + scryfall_id: card.scryfall_card._id, + type_line: card.type_line ? card.type_line : card.scryfall_card.type_line + }) + ) { + editCard({ + headers: { CubeID: cubeID }, + variables: { + cardID: card._id, + componentID: activeComponentID, + ...mutableCardDetails + } + }); + } - if (activeComponentID !== destination) { - deleteCard(card._id, destination); - } + if (activeComponentID !== destination) { + deleteCard(card._id, destination); + } - clear(); - }, - [ - activeComponentID, - card, - clear, - deleteCard, - destination, - editCard, - mutableCardDetails - ] - ); + clear(); + } return ( 0}> {Object.keys(card).length > 0 && (
- {editable ? 'Edit Card' : card.name} + + {editable ? 'Edit Card' : card.name ? card.name : card.scryfall_card.name} + - {card.name} - {card.back_image && ( - {card.name} + { + {!card.scryfall_card.image_uris && ( + {card.scryfall_card.card_faces[1].name} )} @@ -114,7 +124,7 @@ export default function EditCardModal({ card, clear, editable }) { } /> - setMutableCardDetails((prevState) => ({ @@ -122,7 +132,7 @@ export default function EditCardModal({ card, clear, editable }) { scryfall_id: pd.scryfall_id })) } - /> + /> */} @@ -143,17 +153,10 @@ export default function EditCardModal({ card, clear, editable }) { {editable && ( - } - > + }> Submit Changes - } - > + }> Discard Changes diff --git a/src/components/Cube Page/MoveDeleteMenu.jsx b/src/components/Cube Page/MoveDeleteMenu.jsx index 8cfa1ae..63f0cef 100644 --- a/src/components/Cube Page/MoveDeleteMenu.jsx +++ b/src/components/Cube Page/MoveDeleteMenu.jsx @@ -2,23 +2,19 @@ import React from 'react'; import MUIFormControl from '@mui/material/FormControl'; import MUIInputLabel from '@mui/material/InputLabel'; import MUISelect from '@mui/material/Select'; +import MUITypography from '@mui/material/Typography'; +import theme from '../../theme'; import { CubeContext } from '../../contexts/cube-context'; -export default function MoveDeleteMenu({ - destination, - editable, - setDestination -}) { +export default function MoveDeleteMenu({ destination, editable, setDestination }) { const { cubeState: { modules, rotations } } = React.useContext(CubeContext); return ( - - Cube Component - + Cube Component )} - + ); diff --git a/src/components/Deck Page/CardMapItem.jsx b/src/components/Deck Page/CardMapItem.jsx new file mode 100644 index 0000000..a6d0fae --- /dev/null +++ b/src/components/Deck Page/CardMapItem.jsx @@ -0,0 +1,164 @@ +import React, { useCallback, useContext, useState } from 'react'; +import MUIIconButton from '@mui/material/IconButton'; +import MUIMenu from '@mui/material/Menu'; +import MUIMoreVertIcon from '@mui/icons-material/MoreVert'; +import MUITextField from '@mui/material/TextField'; +import MUITypography from '@mui/material/Typography'; +import { useParams } from 'react-router'; + +import HoverPreview from '../miscellaneous/HoverPreview'; +import ManaCostSVGs from '../miscellaneous/ManaCostSVGs'; +import MoveToOption from './MoveToOption'; +import deckComponents from '../../constants/deck-components'; +import setNumberOfDeckCardCopies from '../../graphql/mutations/deck/set-number-of-deck-card-copies'; +import { AuthenticationContext } from '../../contexts/Authentication'; +import { DeckContext } from '../../contexts/deck-context'; + +export default function CardMapItem({ + cardCountState, + component, + scryfall_card, + setCardCountState +}) { + const { userID } = useContext(AuthenticationContext); + const { deckState } = useContext(DeckContext); + const { deckID } = useParams(); + const [anchorEl, setAnchorEl] = useState(); + const open = !!anchorEl; + + const options = deckComponents + .filter((value) => component.field_name !== value.field_name) + .map((value) => ({ ...value, multiple: false })); + + for (let index = 0; index < options.length; index++) { + if ( + !options[index].multiple && + (cardCountState[scryfall_card._id] + ? cardCountState[scryfall_card._id][component.field_name] + : 0) > 1 + ) { + options.splice(index + 1, 0, { + ...options[index], + multiple: true + }); + } + } + + const handleChangeNumberOfCopies = useCallback( + function ({ mainboard_count, maybeboard_count, scryfall_id, sideboard_count }) { + if (deckID) { + setNumberOfDeckCardCopies({ + headers: { DeckID: deckID }, + variables: { + mainboard_count, + maybeboard_count, + scryfall_id, + sideboard_count + } + }); + } + }, + [deckID] + ); + + return ( +
+ setAnchorEl(event.currentTarget)} + > + + + setAnchorEl(null)} + > + {options.map((option) => ( + + ))} + + + handleChangeNumberOfCopies({ + ...cardCountState[scryfall_card._id], + scryfall_id: scryfall_card._id + }), + step: 1, + style: { + paddingBottom: 4, + paddingTop: 4 + } + }} + onChange={(event) => + setCardCountState((prevState) => ({ + ...prevState, + [scryfall_card._id]: { + ...prevState[scryfall_card._id], + [component.field_name]: parseInt(event.target.value) + } + })) + } + style={{ width: 64 }} + type="number" + value={ + cardCountState[scryfall_card._id] + ? cardCountState[scryfall_card._id][component.field_name] + : 0 + } + /> +
+ + + {scryfall_card.name} + + + {scryfall_card._set.toUpperCase()} + + + +
+
+ ); +} diff --git a/src/components/Deck Page/CloneDeckButton.jsx b/src/components/Deck Page/CloneDeckButton.jsx new file mode 100644 index 0000000..5b5f9f1 --- /dev/null +++ b/src/components/Deck Page/CloneDeckButton.jsx @@ -0,0 +1,57 @@ +import React, { useContext, useState } from 'react'; +import MUIButton from '@mui/material/Button'; +import MUICircularProgress from '@mui/material/CircularProgress'; +import MUICloudDoneOutlinedIcon from '@mui/icons-material/CloudDoneOutlined'; +import MUIFileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined'; +import { useNavigate } from 'react-router-dom'; + +import cloneDeck from '../../graphql/mutations/deck/clone-deck'; +import deckQuery from '../../constants/deck-query'; +import { ErrorContext } from '../../contexts/Error'; + +export default function CloneDeckButton({ DeckID }) { + const { setErrorMessages } = useContext(ErrorContext); + const navigate = useNavigate(); + const [cloning, setCloning] = useState(false); + const [success, setSuccess] = useState(false); + + return ( + { + if (!success) { + try { + setCloning(true); + const data = await cloneDeck({ + headers: { DeckID }, + queryString: deckQuery + }); + setSuccess(true); + setTimeout(() => { + navigate(`/deck/${data.data.cloneDeck._id}`, { + state: { deckData: data.data.cloneDeck } + }); + setSuccess(false); + }, 1000); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setCloning(false); + } + } + }} + startIcon={(() => { + if (cloning) { + return ; + } + if (success) { + return ; + } + return ; + })()} + > + Clone Deck + + ); +} diff --git a/src/components/Deck Page/ComponentMapItem.jsx b/src/components/Deck Page/ComponentMapItem.jsx new file mode 100644 index 0000000..6ce2f9f --- /dev/null +++ b/src/components/Deck Page/ComponentMapItem.jsx @@ -0,0 +1,130 @@ +import React, { useContext, useEffect, useState } from 'react'; +import MUICard from '@mui/material/Card'; +import MUICardActions from '@mui/material/CardActions'; +import MUICardContent from '@mui/material/CardContent'; +import MUICardHeader from '@mui/material/CardHeader'; +import MUIGrid from '@mui/material/Grid'; +import MUITypography from '@mui/material/Typography'; +import { useParams } from 'react-router'; + +import TypeMapItem from './TypeMapItem'; +import ScryfallRequest from '../miscellaneous/ScryfallRequest'; +import deckComponents from '../../constants/deck-components'; +import generalCardTypes from '../../constants/general-card-types'; +import setNumberOfDeckCardCopies from '../../graphql/mutations/deck/set-number-of-deck-card-copies'; +import { AuthenticationContext } from '../../contexts/Authentication'; +import { DeckContext } from '../../contexts/deck-context'; + +export default function ComponentMapItem({ component, componentCards }) { + const { userID } = useContext(AuthenticationContext); + const { deckState } = useContext(DeckContext); + const { deckID } = useParams(); + + const [cardCountState, setCardCountState] = useState( + componentCards.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue.scryfall_card._id]: { + mainboard_count: currentValue.mainboard_count, + maybeboard_count: currentValue.maybeboard_count, + sideboard_count: currentValue.sideboard_count + } + }), + {} + ) + ); + + function addCardToComponent(cardData) { + const otherComponents = deckComponents.filter( + (cmpnnt) => cmpnnt.display_name !== component.display_name + ); + const existingCard = deckState.cards.find((card) => card.scryfall_card._id === cardData._id); + if (existingCard) { + const variables = otherComponents.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue.field_name]: existingCard[currentValue.field_name] + }), + { + scryfall_id: cardData._id, + [component.field_name]: existingCard[component.field_name] + 1 + } + ); + setNumberOfDeckCardCopies({ + headers: { DeckID: deckID }, + variables + }); + } else { + const variables = otherComponents.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue.field_name]: 0 + }), + { + scryfall_id: cardData._id, + [component.field_name]: 1 + } + ); + setNumberOfDeckCardCopies({ + headers: { DeckID: deckID }, + variables + }); + } + } + + useEffect(() => { + setCardCountState( + componentCards.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue.scryfall_card._id]: { + mainboard_count: currentValue.mainboard_count, + maybeboard_count: currentValue.maybeboard_count, + sideboard_count: currentValue.sideboard_count + } + }), + {} + ) + ); + }, [componentCards]); + + return ( + + + + {component.display_name} ( + {Object.values(cardCountState).reduce( + (previousValue, currentValue) => previousValue + currentValue[component.field_name], + 0 + )} + ) + + } + /> + + {generalCardTypes.map((generalCardType) => ( + + ))} + + {deckState && deckState.creator._id === userID && ( + + + + )} + + + ); +} diff --git a/src/components/Deck Page/DeckDisplay.jsx b/src/components/Deck Page/DeckDisplay.jsx new file mode 100644 index 0000000..9648b00 --- /dev/null +++ b/src/components/Deck Page/DeckDisplay.jsx @@ -0,0 +1,31 @@ +import React, { useContext } from 'react'; +import MUIGrid from '@mui/material/Grid'; + +import ComponentMapItem from './ComponentMapItem'; +import customSort from '../../functions/custom-sort'; +import deckComponents from '../../constants/deck-components'; +import { DeckContext } from '../../contexts/deck-context'; + +export default function DeckDisplay() { + const { + deckState: { cards } + } = useContext(DeckContext); + const sortedCards = customSort(cards, [ + 'scryfall_card.cmc', + 'scryfall_card.name', + 'scryfall_card._set', + 'scryfall_card.collector_number' + ]); + + return ( + + {deckComponents.map((component) => ( + card[component.field_name] > 0)} + component={component} + key={component.display_name} + /> + ))} + + ); +} diff --git a/src/components/Deck Page/DeckInfo.jsx b/src/components/Deck Page/DeckInfo.jsx index 9da4f15..c4b387c 100644 --- a/src/components/Deck Page/DeckInfo.jsx +++ b/src/components/Deck Page/DeckInfo.jsx @@ -10,7 +10,6 @@ import MUIDialog from '@mui/material/Dialog'; import MUIDialogActions from '@mui/material/DialogActions'; import MUIDialogContent from '@mui/material/DialogContent'; import MUIDialogTitle from '@mui/material/DialogTitle'; -import MUIFileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined'; import MUIFormControl from '@mui/material/FormControl'; import MUIFormControlLabel from '@mui/material/FormControlLabel'; import MUIHelpOutlineIcon from '@mui/icons-material/HelpOutline'; @@ -26,31 +25,35 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import { CSVLink } from 'react-csv'; import { Link } from 'react-router-dom'; +import CloneDeckButton from './CloneDeckButton'; import DeleteDeckForm from '../../forms/DeleteDeckForm'; import ScryfallRequest from '../miscellaneous/ScryfallRequest'; +import editDeck from '../../graphql/mutations/deck/edit-deck'; +import formats from '../../constants/formats'; import generateCSVList from '../../functions/generate-csv-list'; import randomSampleWOReplacement from '../../functions/random-sample-wo-replacement'; import theme from '../../theme'; import { AuthenticationContext } from '../../contexts/Authentication'; import { DeckContext } from '../../contexts/deck-context'; +import { ErrorContext } from '../../contexts/Error'; export default function DeckInfo() { const { isLoggedIn, userID } = useContext(AuthenticationContext); const { + abortControllerRef, deckState: { _id: deckID, + cards, creator, description, format, image, - mainboard, name: deckName, - published, - sideboard - }, - cloneDeck, - editDeck + published + } + // warnings } = useContext(DeckContext); + const { setErrorMessages } = useContext(ErrorContext); const [descriptionInput, setDescriptionInput] = useState(description); const [isPublished, setIsPublished] = useState(published); const [deckNameInput, setDeckNameInput] = useState(deckName); @@ -58,6 +61,19 @@ export default function DeckInfo() { const [sampleHand, setSampleHand] = useState([]); const deckImageWidth = useMediaQuery(theme.breakpoints.up('md')) ? 150 : 75; + function generateSampleHand() { + setSampleHand( + randomSampleWOReplacement( + cards.reduce((previousValue, currentValue) => { + for (let index = 0; index < currentValue.mainboard_count; index++) { + previousValue.push({ ...currentValue.scryfall_card }); + } + }, []), + 7 + ) + ); + } + useEffect(() => { setDeckNameInput(deckName); }, [deckName]); @@ -72,36 +88,31 @@ export default function DeckInfo() { return ( - + - setSampleHand([])} open={sampleHand.length > 0}> - Sample Hand from {name} - - - {sampleHand.map((card) => ( - - {card.name} - - ))} - - - - - setSampleHand(randomSampleWOReplacement(mainboard, 7)) - } - > - New Sample Hand - - - + { + setSampleHand([])} open={sampleHand.length > 0}> + Sample Hand from {deckName} + + + {sampleHand.map((card) => ( + + {card.name} + + ))} + + + + New Sample Hand + + + } { - editDeck({ - description: descriptionInput, - format: event.target.value, - image: image.scryfall_id, - name: deckNameInput, - published: isPublished - }); + onChange={async (event) => { + try { + await editDeck({ + headers: { DeckID: deckID }, + queryString: `{\n_id\nformat\n}`, + signal: abortControllerRef.current.signal, + variables: { format: event.target.value } + }); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } }} value={format} inputProps={{ id: 'format-selector' }} > - - - - - - - - + {formats.map((frmt) => ( + + ))} } avatar={ - image ? ( + image && ( {image.alt} - ) : undefined + ) } title={ { - editDeck({ - description: descriptionInput, - format, - image: image.scryfall_id, - name: deckNameInput, - published: isPublished - }); + onBlur: async () => { + try { + await editDeck({ + headers: { DeckID: deckID }, + queryString: `{\n_id\nname\n}`, + signal: abortControllerRef.current.signal, + variables: { name: deckNameInput } + }); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } } }} label="Deck Name" @@ -179,15 +193,18 @@ export default function DeckInfo() { control={ { - editDeck({ - description: descriptionInput, - format, - image: image.scryfall_id, - name: deckNameInput, - published: !isPublished - }); - setIsPublished((prevState) => !prevState); + onChange={async () => { + try { + await editDeck({ + headers: { DeckID: deckID }, + queryString: `{\n_id\npublished\n}`, + signal: abortControllerRef.current.signal, + variables: { published: !isPublished } + }); + setIsPublished((prevState) => !prevState); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } }} /> } @@ -204,15 +221,10 @@ export default function DeckInfo() { subheader={ - Designed by:{' '} - {creator.name} + Designed by: {creator.name} - + Export to CSV @@ -225,14 +237,17 @@ export default function DeckInfo() { disabled={creator._id !== userID} fullWidth={true} inputProps={{ - onBlur: () => { - editDeck({ - description: descriptionInput, - format, - image: image.scryfall_id, - name: deckNameInput, - published: isPublished - }); + onBlur: async () => { + try { + await editDeck({ + headers: { DeckID: deckID }, + queryString: `{\n_id\ndescription\n}`, + signal: abortControllerRef.current.signal, + variables: { description: descriptionInput } + }); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } } }} label="Deck Description" @@ -247,17 +262,36 @@ export default function DeckInfo() { { - editDeck({ - description: descriptionInput, - format, - image: chosenCard.scryfall_id, - name: deckNameInput, - published: isPublished - }); + onSubmit={async (chosenCard) => { + try { + await editDeck({ + headers: { DeckID: deckID }, + queryString: `{\n_id\nimage\n}`, + signal: abortControllerRef.current.signal, + variables: { image: chosenCard._id } + }); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } }} /> )} + + {/*
+ {warnings.map((warning) => ( + + {warning} + + ))} +
*/} )} - {isLoggedIn && ( - } - > - Clone Deck - - )} - - - setSampleHand(randomSampleWOReplacement(mainboard, 7)) - } - startIcon={} - > + {isLoggedIn && } + }> Sample Hand diff --git a/src/components/Deck Page/MoveToOption.jsx b/src/components/Deck Page/MoveToOption.jsx new file mode 100644 index 0000000..a7c26da --- /dev/null +++ b/src/components/Deck Page/MoveToOption.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import MUIMenuItem from '@mui/material/MenuItem'; + +export default function MoveToOption({ + cardCountState, + component, + handleChangeNumberOfCopies, + option, + scryfall_card, + setAnchorEl +}) { + return ( + { + handleChangeNumberOfCopies({ + ...cardCountState[scryfall_card._id], + [component.field_name]: option.multiple + ? 0 + : cardCountState[scryfall_card._id][component.field_name] - 1, + [option.field_name]: + cardCountState[scryfall_card._id][option.field_name] + + (option.multiple ? cardCountState[scryfall_card._id][component.field_name] : 1), + scryfall_id: scryfall_card._id + }); + setAnchorEl(null); + }} + > + {`Move ${option.multiple ? 'All' : 1} to ${option.display_name}`} + + ); +} diff --git a/src/components/Deck Page/TypeMapItem.jsx b/src/components/Deck Page/TypeMapItem.jsx new file mode 100644 index 0000000..1cddd01 --- /dev/null +++ b/src/components/Deck Page/TypeMapItem.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import MUITypography from '@mui/material/Typography'; + +import CardMapItem from './CardMapItem'; +import specificCardType from '../../functions/specific-card-type'; + +export default function TypeMapItem({ + cardCountState, + component, + componentCards, + generalCardType, + setCardCountState +}) { + const cardsOfType = componentCards.filter( + (card) => specificCardType(card.scryfall_card.type_line) === generalCardType + ); + + return ( + cardsOfType.length > 0 && ( + + + {`${generalCardType} (${cardsOfType.reduce( + (previousValue, currentValue) => + previousValue + + (cardCountState[currentValue.scryfall_card._id] + ? cardCountState[currentValue.scryfall_card._id][component.field_name] + : 0), + 0 + )})`} + + {cardsOfType.map(({ scryfall_card }) => ( + + ))} + + ) + ); +} diff --git a/src/components/Main Navigation/AuthenticateForm.jsx b/src/components/Main Navigation/AuthenticateForm.jsx index 898ee1f..920e720 100644 --- a/src/components/Main Navigation/AuthenticateForm.jsx +++ b/src/components/Main Navigation/AuthenticateForm.jsx @@ -1,4 +1,5 @@ import React, { useContext, useState } from 'react'; +import Cookies from 'js-cookie'; import MUIButton from '@mui/material/Button'; import MUIDialog from '@mui/material/Dialog'; import MUIDialogActions from '@mui/material/DialogActions'; @@ -13,8 +14,13 @@ import MUIVisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutli import MUIVisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import { makeStyles } from '@mui/styles'; +import login from '../../graphql/mutations/account/login'; +import register from '../../graphql/mutations/account/register'; +import requestPasswordReset from '../../graphql/mutations/account/request-password-reset'; +import tokenQuery from '../../constants/token-query'; import LoadingSpinner from '../miscellaneous/LoadingSpinner'; import { AuthenticationContext } from '../../contexts/Authentication'; +import { ErrorContext } from '../../contexts/Error'; const useStyles = makeStyles({ activeTab: { @@ -31,9 +37,9 @@ const useStyles = makeStyles({ }); export default function AuthenticateForm({ open, toggleOpen }) { - const { loading, login, register, requestPasswordReset } = useContext( - AuthenticationContext - ); + const { abortControllerRef, loading, setLoading, setUserInfo } = + useContext(AuthenticationContext); + const { setErrorMessages } = useContext(ErrorContext); const classes = useStyles(); const [selectedTab, setSelectedTab] = useState(0); const [emailInput, setEmailInput] = useState(''); @@ -41,19 +47,107 @@ export default function AuthenticateForm({ open, toggleOpen }) { const [passwordInput, setPasswordInput] = useState(''); const [passwordVisible, setPasswordVisible] = useState(false); - function submitForm(event) { + async function submitForm(event) { event.preventDefault(); if (selectedTab === 0) { - login(emailInput, passwordInput); + try { + setLoading(true); + const { + data: { + login: { + _id, + admin, + avatar, + buds, + conversations, + measurement_system, + name, + radius, + token + } + } + } = await login({ + queryString: tokenQuery, + signal: abortControllerRef.current.signal, + variables: { email: emailInput, password: passwordInput } + }); + setUserInfo({ + admin, + avatar, + buds, + conversations, + measurement_system, + radius, + userID: _id, + userName: name + }); + Cookies.set('authentication_token', token); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setLoading(false); + } } if (selectedTab === 1) { - requestPasswordReset(emailInput); + try { + setLoading(true); + await requestPasswordReset({ + signal: abortControllerRef.current.signal, + variables: { email: emailInput } + }); + setErrorMessages((prevState) => { + return [ + ...prevState, + 'A link to reset your password has been sent to the provided email address. Please allow a few minutes and check both your inbox and your spam folder.' + ]; + }); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setLoading(false); + } } if (selectedTab === 2) { - register(emailInput, nameInput, passwordInput); + try { + setLoading(true); + const { + data: { + register: { + _id, + admin, + avatar, + buds, + conversations, + measurement_system, + name, + radius, + token + } + } + } = await register({ + queryString: tokenQuery, + signal: abortControllerRef.current.signal, + variables: { email: emailInput, name: nameInput, password: passwordInput } + }); + setUserInfo({ + admin, + avatar, + buds, + conversations, + measurement_system, + radius, + userID: _id, + userName: name + }); + Cookies.set('authentication_token', token); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setLoading(false); + } } toggleOpen(); @@ -121,9 +215,7 @@ export default function AuthenticateForm({ open, toggleOpen }) { - setPasswordVisible((prevState) => !prevState) - } + onClick={() => setPasswordVisible((prevState) => !prevState)} > {passwordVisible ? ( @@ -202,9 +294,7 @@ export default function AuthenticateForm({ open, toggleOpen }) { - setPasswordVisible((prevState) => !prevState) - } + onClick={() => setPasswordVisible((prevState) => !prevState)} > {passwordVisible ? ( diff --git a/src/components/Main Navigation/ChatDialog.jsx b/src/components/Main Navigation/ChatDialog.jsx new file mode 100644 index 0000000..7fe7b0a --- /dev/null +++ b/src/components/Main Navigation/ChatDialog.jsx @@ -0,0 +1,184 @@ +import React, { useContext, useRef, useState } from 'react'; +import MUIAddCommentOutlinedIcon from '@mui/icons-material/AddCommentOutlined'; +import MUIButton from '@mui/material/Button'; +import MUIDialog from '@mui/material/Dialog'; +import MUIDialogActions from '@mui/material/DialogActions'; +import MUIDialogContent from '@mui/material/DialogContent'; +import MUIDialogTitle from '@mui/material/DialogTitle'; +import MUIPaper from '@mui/material/Paper'; +import MUITextField from '@mui/material/TextField'; +import MUITypography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; + +import Avatar from '../miscellaneous/Avatar'; +import ParticipantsInput from './ParticipantsInput'; +import createConversationMessage from '../../graphql/mutations/conversation/create-conversation-message'; +import { AuthenticationContext } from '../../contexts/Authentication'; +import { ErrorContext } from '../../contexts/Error'; +import { primaryColor, secondaryColor } from '../../theme'; + +const useStyles = makeStyles({ + messageDialog: { + display: 'flex', + flexDirection: 'column', + maxHeight: '90vh' + }, + messageDialogActions: { + alignItems: 'stretch', + columnGap: 8, + flexDirection: 'row' + }, + messageDialogContent: { + flexGrow: 1, + marginRight: 16, + overflowY: 'auto', + display: 'flex', + flexDirection: 'column-reverse' + }, + messageLI: { + display: 'flex', + margin: '4px 0' + } +}); + +export default function ChatDialog({ + close, + conversation, + open, + setNewConversationParticipants, + setSelectedConversationID +}) { + if (!conversation) return null; + + const { abortControllerRef, userID } = useContext(AuthenticationContext); + const { setErrorMessages } = useContext(ErrorContext); + const newMessageRef = useRef(); + const [newMessageText, setNewMessageText] = useState(''); + const { messageDialog, messageDialogActions, messageDialogContent, messageLI } = useStyles(); + + const { _id, messages, participants } = conversation; + participants.sort((a, b) => { + if (a._id === userID) return -1; + if (b._id === userID) return 1; + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + return 1; + }); + + return ( + + + + + +
    + {messages.map((message) => ( +
  • + + + {message.body.split('\n').map((subString, index) => ( + + {subString} + + ))} + + {new Date(parseInt(message.createdAt)).toLocaleString()} + + +
  • + ))} +
+
+ + { + if (event.target.value !== '\n') { + setNewMessageText(event.target.value); + } + }} + onKeyDown={async (event) => { + event.persist(); + try { + if (!event.shiftKey && event.key === 'Enter' && newMessageText.length > 0) { + const response = await createConversationMessage({ + headers: _id ? { ConversationID: _id } : undefined, + queryString: `{\n_id\n}`, + signal: abortControllerRef.current.signal, + variables: { + body: newMessageText, + participants: participants.map((participant) => participant._id) + } + }); + + if (_id) { + setNewMessageText(''); + newMessageRef.current.focus(); + } else { + // setNewConversationParticipants([]); + setSelectedConversationID(response.data.createConversationMessage._id); + } + } + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } + }} + rows={2} + type="text" + value={newMessageText} + /> + { + try { + if (newMessageText.length > 0) { + const response = await createConversationMessage({ + headers: _id ? { ConversationID: _id } : undefined, + queryString: `{\n_id\n}`, + signal: abortControllerRef.current.signal, + variables: { + body: newMessageText, + participants: participants.map((participant) => participant._id) + } + }); + + if (_id) { + setNewMessageText(''); + newMessageRef.current.focus(); + } else { + // setNewConversationParticipants([]); + setSelectedConversationID(response.data.createConversationMessage._id); + } + } + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } + }} + startIcon={} + > + Send + + +
+ ); +} diff --git a/src/components/Main Navigation/ChatDrawer.jsx b/src/components/Main Navigation/ChatDrawer.jsx new file mode 100644 index 0000000..a182226 --- /dev/null +++ b/src/components/Main Navigation/ChatDrawer.jsx @@ -0,0 +1,215 @@ +import React, { useContext, useEffect, useState } from 'react'; +import MUIAddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import MUIAvatarGroup from '@mui/material/AvatarGroup'; +import MUICard from '@mui/material/Card'; +import MUICardContent from '@mui/material/CardContent'; +import MUICardHeader from '@mui/material/CardHeader'; +import MUIDrawer from '@mui/material/Drawer'; +import MUITypography from '@mui/material/Typography'; +import { makeStyles } from '@mui/styles'; + +import Avatar from '../miscellaneous/Avatar'; +import ChatDialog from './ChatDialog'; +import ParticipantsInput from './ParticipantsInput'; +import { AuthenticationContext } from '../../contexts/Authentication'; + +const useStyles = makeStyles({ + columnFlex: { + display: 'flex', + flexDirection: 'column', + rowGap: 8 + }, + conversationCard: { + backgroundColor: 'transparent', + border: '2px solid white', + cursor: 'pointer', + margin: 0 + } +}); + +function ConversationMapItem({ conversation, setSelectedConversationID }) { + const { userID } = useContext(AuthenticationContext); + const lastMessage = conversation.messages[conversation.messages.length - 1]; + const { conversationCard } = useStyles(); + + conversation.participants.sort((a, b) => { + if (a._id === userID) return -1; + if (b._id === userID) return 1; + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + return 1; + }); + + return ( + setSelectedConversationID(conversation._id)} + > + + {conversation.participants + .filter((participant) => participant._id !== userID) + .map((participant) => ( + + ))} + + } + style={{ backgroundColor: 'transparent' }} + title={ + + {conversation.participants + .filter((participant) => participant._id !== userID) + .map((participant) => participant.name) + .join(', ')} + + } + /> + + + {lastMessage.body.substring(0, 100).concat(lastMessage.body.length > 100 ? '...' : '')} + + + {`— ${lastMessage.author.name}, ${new Date( + parseInt(lastMessage.createdAt) + ).toLocaleString()}`} + + + + ); +} + +export default function ChatDrawer({ chatDrawerOpen, setChatDrawerOpen }) { + const { avatar, conversations, userID, userName } = useContext(AuthenticationContext); + const [existingConversationParticipants, setExistingConversationParticipants] = useState([]); + const [newConversationParticipants, setNewConversationParticipants] = useState([]); + const [selectedConversationID, setSelectedConversationID] = useState(); + const { columnFlex, conversationCard } = useStyles(); + + const filteredConversations = conversations.filter((conversation) => + existingConversationParticipants.every((ecp) => + conversation.participants.some((cp) => ecp._id === cp._id) + ) + ); + + useEffect(() => { + const existingConversation = conversations.find( + (conversation) => + conversation.participants.length === newConversationParticipants.length && + conversation.participants.every((cp) => + newConversationParticipants.some((ncp) => ncp._id === cp._id) + ) + ); + + if (existingConversation) { + setSelectedConversationID(existingConversation._id); + } else { + setSelectedConversationID(null); + } + }, [newConversationParticipants]); + + return ( + + { + setNewConversationParticipants([]); + setSelectedConversationID(null); + }} + conversation={(() => { + if (selectedConversationID) { + return conversations.find( + (conversation) => conversation._id === selectedConversationID + ); + } + if (newConversationParticipants.length > 0) { + return { messages: [], participants: newConversationParticipants }; + } + return null; + })()} + open={!!selectedConversationID || newConversationParticipants.length > 0} + setNewConversationParticipants={setNewConversationParticipants} + setSelectedConversationID={setSelectedConversationID} + /> + + setChatDrawerOpen(false)} + open={chatDrawerOpen} + > +
+ +
+
+ {!filteredConversations.find( + (fc) => fc.participants.length - 1 === existingConversationParticipants.length + ) && ( + { + setExistingConversationParticipants([]); + setNewConversationParticipants([ + ...existingConversationParticipants, + { _id: userID, avatar, name: userName } + ]); + }} + > + + {existingConversationParticipants.map((participant) => ( + + ))} + + } + style={{ backgroundColor: 'transparent' }} + title={ + + {existingConversationParticipants + .map((participant) => participant.name) + .join(', ')} + + } + /> + + + + New Conversation + + + + )} + {filteredConversations + .sort((a, b) => { + if ( + parseInt(a.messages[a.messages.length - 1].createdAt) > + parseInt(b.messages[b.messages.length - 1].createdAt) + ) { + return -1; + } + return 1; + }) + .map((conversation) => ( + + ))} +
+
+
+
+
+ ); +} diff --git a/src/components/Main Navigation/Navigation.jsx b/src/components/Main Navigation/Navigation.jsx index 791397a..29157b2 100644 --- a/src/components/Main Navigation/Navigation.jsx +++ b/src/components/Main Navigation/Navigation.jsx @@ -1,13 +1,14 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useContext, useState } from 'react'; +import Cookies from 'js-cookie'; import MUIAccountCircleIcon from '@mui/icons-material/AccountCircle'; import MUIAppBar from '@mui/material/AppBar'; -import MUIButton from '@mui/material/Button'; -import MUIDownloadIcon from '@mui/icons-material/Download'; -import MUIDrawer from '@mui/material/Drawer'; +import MUIBadge from '@mui/material/Badge'; +import MUIChatOutlinedIcon from '@mui/icons-material/ChatOutlined'; import MUIIconButton from '@mui/material/IconButton'; import MUIToolbar from '@mui/material/Toolbar'; import MUITooltip from '@mui/material/Tooltip'; import MUITypography from '@mui/material/Typography'; +import MUILogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined'; import MUIMenuIcon from '@mui/icons-material/Menu'; import useMediaQuery from '@mui/material/useMediaQuery'; import { Link } from 'react-router-dom'; @@ -15,22 +16,33 @@ import { makeStyles } from '@mui/styles'; import AuthenticateForm from './AuthenticateForm'; import Avatar from '../miscellaneous/Avatar'; -import NavigationLinks from './NavigationLinks'; +import ChatDrawer from './ChatDrawer'; +import NavigationDrawer from './NavigationDrawer'; import SiteSearchBar from './SiteSearchBar'; +import logoutSingleDevice from '../../graphql/mutations/account/logout-single-device'; import theme from '../../theme'; import { AuthenticationContext } from '../../contexts/Authentication'; -import { PermissionsContext } from '../../contexts/Permissions'; +import { ErrorContext } from '../../contexts/Error'; const useStyles = makeStyles({ appBar: { background: `linear-gradient(to right, ${theme.palette.primary.main}, calc(2/3 * 100%), ${theme.palette.secondary.main})` }, - drawer: { - '& .MuiPaper-root': { - background: `linear-gradient(to bottom, ${theme.palette.primary.main}, calc(2/3 * 100%), ${theme.palette.secondary.main})`, - margin: 0 + badge: { + '& > .MuiBadge-badge': { + border: '2px solid white', + borderRadius: '100%', + color: 'white', + cursor: 'pointer', + height: 38, + padding: 8, + width: 38 } }, + badgeIcon: { + height: 26, + width: 26 + }, leftContainer: { alignItems: 'center', display: 'flex', @@ -58,41 +70,29 @@ const useStyles = makeStyles({ }); export default function Navigation() { - const { isLoggedIn, avatar, userID, userName } = useContext( - AuthenticationContext - ); - const { deferredPrompt, setDeferredPrompt } = useContext(PermissionsContext); - const searchBarLocation = useMediaQuery(theme.breakpoints.up('md')) - ? 'top' - : 'side'; - const [authenticateFormDisplayed, setAuthenticateFormDisplayed] = - useState(false); - const [drawerOpen, setDrawerOpen] = useState(false); + const { abortControllerRef, avatar, isLoggedIn, setLoading, setUserInfo, userID, userName } = + useContext(AuthenticationContext); + const { setErrorMessages } = useContext(ErrorContext); + const searchBarLocation = useMediaQuery(theme.breakpoints.up('md')) ? 'top' : 'side'; + const [authenticateFormDisplayed, setAuthenticateFormDisplayed] = useState(false); + const [chatDrawerOpen, setChatDrawerOpen] = useState(false); + const [navigationDrawerOpen, setNavigationDrawerOpen] = useState(false); const classes = useStyles(); - function toggleDrawer(event) { - if ( - event.type === 'keydown' && - (event.key === 'Tab' || event.key === 'Shift') - ) { - return; - } - setDrawerOpen((prevState) => !prevState); - } - return ( setAuthenticateFormDisplayed(false)} /> +
setDrawerOpen(true)} + onClick={() => setNavigationDrawerOpen(true)} /> Cube Level Midnight @@ -100,12 +100,75 @@ export default function Navigation() {
{searchBarLocation === 'top' && ( - + )} {isLoggedIn ? ( - - - +
+ } + className={classes.badge} + color="primary" + onClick={(event) => { + if (event.target.closest('span').classList.contains('MuiBadge-colorPrimary')) { + setChatDrawerOpen(true); + } + }} + overlap="circular" + > + } + className={classes.badge} + color="secondary" + onClick={async (event) => { + event.persist(); + if ( + event.target.closest('span').classList.contains('MuiBadge-colorSecondary') + ) { + try { + setLoading(true); + await logoutSingleDevice({ signal: abortControllerRef.current.signal }); + Cookies.remove('authentication_token'); + setUserInfo({ + admin: false, + avatar: { + card_faces: [], + image_uris: null + }, + measurement_system: 'imperial', + radius: 10, + userID: null, + userName: null + }); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setLoading(false); + } + } + }} + overlap="circular" + > + + + + + +
) : ( - setDrawerOpen(false)} - open={drawerOpen} - > - {searchBarLocation === 'side' && ( - - )} - - {deferredPrompt && ( - { - deferredPrompt.prompt(); - await deferredPrompt.userChoice; - setDeferredPrompt(null); - setDrawerOpen(false); - }} - startIcon={} - > - Install the App! - - )} - + + + + ); diff --git a/src/components/Main Navigation/NavigationDrawer.jsx b/src/components/Main Navigation/NavigationDrawer.jsx new file mode 100644 index 0000000..a3c9eac --- /dev/null +++ b/src/components/Main Navigation/NavigationDrawer.jsx @@ -0,0 +1,50 @@ +import React, { useContext } from 'react'; +import MUIButton from '@mui/material/Button'; +import MUIDownloadIcon from '@mui/icons-material/Download'; +import MUIDrawer from '@mui/material/Drawer'; +import useMediaQuery from '@mui/material/useMediaQuery'; + +import NavigationLinks from './NavigationLinks'; +import SiteSearchBar from './SiteSearchBar'; +import theme from '../../theme'; +import { PermissionsContext } from '../../contexts/Permissions'; + +export default function NavigationDrawer({ navigationDrawerOpen, setNavigationDrawerOpen }) { + const { deferredPrompt, setDeferredPrompt } = useContext(PermissionsContext); + const searchBarLocation = useMediaQuery(theme.breakpoints.up('md')) ? 'top' : 'side'; + + function toggleDrawer(event) { + if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) { + return; + } + setNavigationDrawerOpen((prevState) => !prevState); + } + + return ( + setNavigationDrawerOpen(false)} + open={navigationDrawerOpen} + > + {searchBarLocation === 'side' && ( + + )} + + {deferredPrompt && ( + { + deferredPrompt.prompt(); + await deferredPrompt.userChoice; + setDeferredPrompt(null); + setNavigationDrawerOpen(false); + }} + startIcon={} + > + Install the App! + + )} + + ); +} diff --git a/src/components/Main Navigation/NavigationLinks.jsx b/src/components/Main Navigation/NavigationLinks.jsx index f4c83da..d159951 100644 --- a/src/components/Main Navigation/NavigationLinks.jsx +++ b/src/components/Main Navigation/NavigationLinks.jsx @@ -1,19 +1,15 @@ -import React, { useContext } from 'react'; +import React from 'react'; import MUIAllInclusiveIcon from '@mui/icons-material/AllInclusive'; import MUIArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined'; -// import MUIChatOutlinedIcon from '@mui/icons-material/ChatOutlined'; import MUIHelpOutlineIcon from '@mui/icons-material/HelpOutline'; import MUIHomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; import MUIList from '@mui/material/List'; import MUIListItem from '@mui/material/ListItem'; import MUIListItemIcon from '@mui/material/ListItemIcon'; import MUIListItemText from '@mui/material/ListItemText'; -import MUILogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined'; import { makeStyles } from '@mui/styles'; import { useNavigate } from 'react-router-dom'; -import { AuthenticationContext } from '../../contexts/Authentication'; - const useStyles = makeStyles({ item: { color: '#fff', @@ -28,7 +24,6 @@ const useStyles = makeStyles({ }); export default function NavigationLinks({ toggleDrawer }) { - const { isLoggedIn, logout } = useContext(AuthenticationContext); const classes = useStyles(); const navigate = useNavigate(); @@ -55,26 +50,12 @@ export default function NavigationLinks({ toggleDrawer }) { } ]; - if (isLoggedIn) { - options.push({ - icon: , - name: 'Logout', - onClick: logout - }); - } - return ( - + {options.map(function (option) { return ( - - {option.icon} - + {option.icon} ); diff --git a/src/components/Main Navigation/ParticipantsInput.jsx b/src/components/Main Navigation/ParticipantsInput.jsx new file mode 100644 index 0000000..c0a3cc2 --- /dev/null +++ b/src/components/Main Navigation/ParticipantsInput.jsx @@ -0,0 +1,127 @@ +import React, { useContext, useState } from 'react'; +import MUIAvatar from '@mui/material/Avatar'; +import MUIAutocomplete from '@mui/material/Autocomplete'; +import MUICheckbox from '@mui/material/Checkbox'; +import MUIChip from '@mui/material/Chip'; +import MUIFormControlLabel from '@mui/material/FormControlLabel'; +import MUISearchIcon from '@mui/icons-material/Search'; +import MUITextField from '@mui/material/TextField'; +import { alpha } from '@mui/material/styles'; +import { makeStyles } from '@mui/styles'; + +import theme from '../../theme'; +import { AuthenticationContext } from '../../contexts/Authentication'; + +const useStyles = makeStyles({ + textfield: { + margin: 8, + // minWidth: 300, + width: 'calc(100% - 16px)', + '& input[type=text]': { + color: '#ffffff' + } + }, + autocomplete: { + backgroundColor: alpha(theme.palette.common.white, 0.15), + borderRadius: theme.shape.borderRadius, + // color: '#fff', + // position: 'relative', + '&:hover': { + backgroundColor: alpha(theme.palette.common.white, 0.25) + } + } +}); + +export default function ParticipantsInput({ faded, participants, setParticipants }) { + const { buds, userID } = useContext(AuthenticationContext); + const [filterText, setFilterText] = useState(''); + const { autocomplete, textfield } = useStyles(); + + return ( + option.name} + id="bud-include-input" + inputValue={filterText} + isOptionEqualToValue={(option, value) => option._id === value._id} + multiple + onChange={function (event, value) { + setParticipants(value); + }} + onInputChange={(event, newInputValue) => { + setFilterText(newInputValue); + }} + options={buds.sort((a, b) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + return 1; + })} + renderInput={(params) => ( + setFilterText(event.target.value)} + InputProps={{ + ...params.InputProps, + startAdornment: ( + + + {params.InputProps.startAdornment} + + ) + }} + /> + )} + renderOption={(props, option, { selected }) => ( +
  • + } + label={ + + } + /> + {option.name} +
  • + )} + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => ( + + } + color="primary" + disabled={option._id === userID} + onDelete={ + option._id === userID + ? undefined + : () => { + setParticipants((prevState) => prevState.filter((p) => p._id !== option._id)); + } + } + /> + )) + } + value={participants} + /> + ); +} diff --git a/src/components/Main Navigation/SiteSearchBar.jsx b/src/components/Main Navigation/SiteSearchBar.jsx index 81a5da7..d52d503 100644 --- a/src/components/Main Navigation/SiteSearchBar.jsx +++ b/src/components/Main Navigation/SiteSearchBar.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useRef, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import MUIAutocomplete from '@mui/material/Autocomplete'; import MUICircularProgress from '@mui/material/CircularProgress'; import MUISearchIcon from '@mui/icons-material/Search'; @@ -11,7 +11,6 @@ import { useNavigate } from 'react-router'; import theme from '../../theme'; import useRequest from '../../hooks/request-hook'; import Avatar from '../miscellaneous/Avatar'; -import { CardCacheContext } from '../../contexts/CardCache'; const useStyles = makeStyles({ input: { @@ -41,51 +40,14 @@ const useStyles = makeStyles({ } }); -export default function SiteSearchBar({ color, setDrawerOpen }) { +export default function SiteSearchBar({ color, setNavigationDrawerOpen }) { const searchInput = useRef(); - const { addCardsToCache, scryfallCardDataCache } = - useContext(CardCacheContext); const { loading, sendRequest } = useRequest(); const [searchResults, setSearchResults] = useState([]); const [timer, setTimer] = useState(); const classes = useStyles(); const navigate = useNavigate(); - const updateSearchResults = useCallback( - async function (data) { - const cardSet = new Set(); - - for (const result of data) { - if ( - (result.__typename === 'CubeType' || - result.__typename === 'DeckType') && - result.image - ) { - cardSet.add(result.image); - } - } - - await addCardsToCache([...cardSet]); - - for (const result of data) { - if ( - (result.__typename === 'CubeType' || - result.__typename === 'DeckType') && - result.image - ) { - result.image = { - alt: scryfallCardDataCache.current[result.image].name, - scryfall_id: result.image, - src: scryfallCardDataCache.current[result.image].art_crop - }; - } - } - - setSearchResults(data); - }, - [addCardsToCache] - ); - const searchSite = useCallback( (event) => { event.persist(); @@ -95,10 +57,11 @@ export default function SiteSearchBar({ color, setDrawerOpen }) { setSearchResults([]); } else { await sendRequest({ - callback: updateSearchResults, + callback: setSearchResults, load: true, operation: 'searchSite', get body() { + // https://www.rakeshjesadiya.com/graphql-fields-conflict-return-conflicting-types-use-different-aliases-on-the-fields/ return { query: ` query { @@ -108,37 +71,97 @@ export default function SiteSearchBar({ color, setDrawerOpen }) { __typename } ... on AccountType { - avatar + avatar { + card_faces { + image_uris { + art_crop + } + } + image_uris { + art_crop + } + } name } ... on BlogPostType { - image + stringImage: image title subtitle } ... on CubeType { creator { _id - avatar + avatar { + card_faces { + image_uris { + art_crop + } + } + image_uris { + art_crop + } + } + name + } + cardImage: image { + _id + image_uris { + art_crop + } name + card_faces { + image_uris { + art_crop + } + name + } } - image name } ... on DeckType { creator { _id - avatar + avatar { + card_faces { + image_uris { + art_crop + } + } + image_uris { + art_crop + } + } name } - image + cardImage: image { + _id + image_uris { + art_crop + } + name + card_faces { + image_uris { + art_crop + } + name + } + } name } ... on EventType { createdAt host { _id - avatar + avatar { + card_faces { + image_uris { + art_crop + } + } + image_uris { + art_crop + } + } name } name @@ -191,7 +214,7 @@ export default function SiteSearchBar({ color, setDrawerOpen }) { .getElementsByClassName('MuiAutocomplete-clearIndicator')[0] .click(); setSearchResults([]); - setDrawerOpen(false); + setNavigationDrawerOpen(false); }, 0); } }} @@ -224,7 +247,7 @@ export default function SiteSearchBar({ color, setDrawerOpen }) { {option.__typename === 'AccountType' && (
  • - + User @@ -240,7 +263,8 @@ export default function SiteSearchBar({ color, setDrawerOpen }) { {option.subtitle} @@ -248,9 +272,7 @@ export default function SiteSearchBar({ color, setDrawerOpen }) { Blog Post - - {option.title} - + {option.title}
  • @@ -259,14 +281,33 @@ export default function SiteSearchBar({ color, setDrawerOpen }) { {option.__typename === 'CubeType' && (
  • - {option.image && ( - {option.image.alt} - )} + { + /* option.image */ option.cardImage && ( + { + ) + } Cube @@ -280,10 +321,17 @@ export default function SiteSearchBar({ color, setDrawerOpen }) { {option.__typename === 'DeckType' && (
  • - {option.image && ( + {option.cardImage && ( {option.image.alt} diff --git a/src/components/Match Page/Intermission.jsx b/src/components/Match Page/Intermission.jsx index dd5b5de..74b163c 100644 --- a/src/components/Match Page/Intermission.jsx +++ b/src/components/Match Page/Intermission.jsx @@ -3,16 +3,14 @@ import MUIButton from '@mui/material/Button'; import MUITypography from '@mui/material/Typography'; import ConfirmationDialog from '../miscellaneous/ConfirmationDialog'; -import DeckDisplay from '../miscellaneous/DeckDisplay'; +// import DeckDisplay from '../miscellaneous/DeckDisplay'; import { AuthenticationContext } from '../../contexts/Authentication'; import { MatchContext } from '../../contexts/match-context'; export default function Intermission() { - const [confirmationDialogVisible, setConfirmationDialogVisible] = - React.useState(false); + const [confirmationDialogVisible, setConfirmationDialogVisible] = React.useState(false); const { userID } = React.useContext(AuthenticationContext); - const { matchState, ready, toggleMainboardSideboardMatch } = - React.useContext(MatchContext); + const { matchState, ready, toggleMainboardSideboardMatch } = React.useContext(MatchContext); const player = matchState.players.find((plr) => plr.account._id === userID); @@ -22,13 +20,10 @@ export default function Intermission() { confirmHandler={ready} open={confirmationDialogVisible} title="Are you readier than SpongeBob SquarePants with a belly full of Krabby Patties?" - toggleOpen={() => - setConfirmationDialogVisible((prevState) => !prevState) - } + toggleOpen={() => setConfirmationDialogVisible((prevState) => !prevState)} > - Think of how embarrassing it will be if you get mushroom stamped! Oh, - the shame! + Think of how embarrassing it will be if you get mushroom stamped! Oh, the shame! @@ -41,11 +36,11 @@ export default function Intermission() { Ready! - + /> */} ) : (
    diff --git a/src/components/Match Page/PlayerInfo.jsx b/src/components/Match Page/PlayerInfo.jsx index 5b7913d..750f595 100644 --- a/src/components/Match Page/PlayerInfo.jsx +++ b/src/components/Match Page/PlayerInfo.jsx @@ -47,12 +47,8 @@ export default function PlayerInfo({ player, position, setClickedPlayer }) { const classes = useStyles(); const [dragging, setDragging] = React.useState(false); const { userID } = React.useContext(AuthenticationContext); - const { - adjustEnergyCounters, - adjustLifeTotal, - adjustPoisonCounters, - setNumberInputDialogInfo - } = React.useContext(MatchContext); + const { adjustEnergyCounters, adjustLifeTotal, adjustPoisonCounters, setNumberInputDialogInfo } = + React.useContext(MatchContext); React.useEffect(() => { function energyBadgeClickListner() { @@ -196,10 +192,9 @@ export default function PlayerInfo({ player, position, setClickedPlayer }) { showZero > diff --git a/src/components/miscellaneous/AutoScrollMessages.jsx b/src/components/miscellaneous/AutoScrollMessages.jsx index fd24189..a5a24a2 100644 --- a/src/components/miscellaneous/AutoScrollMessages.jsx +++ b/src/components/miscellaneous/AutoScrollMessages.jsx @@ -42,11 +42,7 @@ const useStyles = makeStyles({ } }); -export default function AutoScrollMessages({ - messages, - submitFunction, - title -}) { +export default function AutoScrollMessages({ messages, submitFunction, title }) { const { isLoggedIn, userID } = useContext(AuthenticationContext); const newMessageRef = useRef(); const [newMessageText, setNewMessageText] = useState(''); @@ -61,9 +57,7 @@ export default function AutoScrollMessages({ return ( - {title}} - /> + {title}} />
      {messages @@ -73,21 +67,14 @@ export default function AutoScrollMessages({ className={messageLI} key={message._id} style={{ - flexDirection: - message.author._id === userID ? 'row-reverse' : 'row' + flexDirection: message.author._id === userID ? 'row-reverse' : 'row' }} > - + { - if ( - !event.shiftKey && - event.key === 'Enter' && - newMessageText.length > 0 - ) { + if (!event.shiftKey && event.key === 'Enter' && newMessageText.length > 0) { submitFunction(newMessageText); setNewMessageText(''); newMessageRef.current.focus(); diff --git a/src/components/miscellaneous/Avatar.jsx b/src/components/miscellaneous/Avatar.jsx index 78bd7a4..fc9fe11 100644 --- a/src/components/miscellaneous/Avatar.jsx +++ b/src/components/miscellaneous/Avatar.jsx @@ -22,15 +22,18 @@ const useStyles = makeStyles({ } }); -export default function LargeAvatar({ alt, size, ...rest }) { +export default function LargeAvatar({ profile, size, ...rest }) { const classes = useStyles(); return ( - + diff --git a/src/components/miscellaneous/DeckDisplay.jsx b/src/components/miscellaneous/DeckDisplay.jsx deleted file mode 100644 index ac594eb..0000000 --- a/src/components/miscellaneous/DeckDisplay.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; -import MUICard from '@mui/material/Card'; -import MUICardContent from '@mui/material/CardContent'; -import MUICardHeader from '@mui/material/CardHeader'; -import MUIGrid from '@mui/material/Grid'; -import MUITypography from '@mui/material/Typography'; - -import customSort from '../../functions/custom-sort'; -import specificCardType from '../../functions/specific-card-type'; -import PlaysetDisplay from './PlaysetDisplay'; - -export default function DeckDisplay({ - add = () => { - // default: don't do anything - }, - authorizedID, - deck, - remove = () => { - // default: don't do anything - }, - toggle = () => { - // default: don't do anything - } -}) { - return ( - - {['Mainboard', 'Sideboard'].map((component) => ( - - - - {component} ({deck[component.toLowerCase()].length}) - - } - /> - - {[ - 'Land', - 'Creature', - 'Planeswalker', - 'Artifact', - 'Enchantment', - 'Instant', - 'Sorcery' - ].map(function (type) { - const group = customSort(deck[component.toLocaleLowerCase()], [ - 'cmc', - 'name', - 'set' - ]).filter((card) => specificCardType(card.type_line) === type); - const condensedGroup = []; - - for (const card of group) { - const existingCopies = condensedGroup.find( - (abstraction) => - abstraction.card.scryfall_id === card.scryfall_id - ); - if (existingCopies) { - existingCopies.copies.push(card._id); - } else { - condensedGroup.push({ - card: { - back_image: card.back_image, - cmc: card.cmc, - collector_number: card.collector_number, - color_identity: card.color_identity, - image: card.image, - keywords: card.keywords, - mana_cost: card.mana_cost, - mtgo_id: card.mtgo_id, - name: card.name, - oracle_id: card.oracle_id, - scryfall_id: card.scryfall_id, - set: card.set, - set_name: card.set_name, - tcgplayer_id: card.tcgplayer_id, - type_line: card.type_line - }, - copies: [card._id] - }); - } - } - - return ( - group.length > 0 && ( - - {`${type} (${group.length})`} - {condensedGroup.map((playset) => ( - - ))} - - ) - ); - })} - - - - ))} - - ); -} diff --git a/src/components/miscellaneous/HoverPreview.jsx b/src/components/miscellaneous/HoverPreview.jsx index 6026746..c467509 100644 --- a/src/components/miscellaneous/HoverPreview.jsx +++ b/src/components/miscellaneous/HoverPreview.jsx @@ -10,7 +10,7 @@ const useStyles = makeStyles({ hoverPreviewImage: { borderRadius: 8, display: 'inline', - height: 264 + height: 350 } }); @@ -47,12 +47,7 @@ export default function HoverPreview({ back_image, children, image }) { right = undefined; } else { left = undefined; - right = `${ - windowWidth - - event.pageX - - hpcWidth + - (hpcWidth * event.pageX) / windowWidth - }px`; + right = `${windowWidth - event.pageX - hpcWidth + (hpcWidth * event.pageX) / windowWidth}px`; } if (event.screenY < windowHeight / 2) { @@ -103,17 +98,9 @@ export default function HoverPreview({ back_image, children, image }) { top: preview.top }} > - front of card + front of card {back_image && ( - back of card + back of card )}
    , document.getElementById('hover-preview') diff --git a/src/components/miscellaneous/PlaysetDisplay.jsx b/src/components/miscellaneous/PlaysetDisplay.jsx deleted file mode 100644 index d835d11..0000000 --- a/src/components/miscellaneous/PlaysetDisplay.jsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from 'react'; -import MUIIconButton from '@mui/material/IconButton'; -import MUISwapHorizIcon from '@mui/icons-material/SwapHoriz'; -import MUISwapVertIcon from '@mui/icons-material/SwapVert'; -import MUITextField from '@mui/material/TextField'; -import MUITooltip from '@mui/material/Tooltip'; -import MUITypography from '@mui/material/Typography'; -import { makeStyles } from '@mui/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { useParams } from 'react-router'; - -import theme, { backgroundColor } from '../../theme'; -import HoverPreview from '../miscellaneous/HoverPreview'; -import ManaCostSVGs from '../miscellaneous/ManaCostSVGs'; -import { AuthenticationContext } from '../../contexts/Authentication'; -import addBasics from '../../graphql/mutations/event/add-basics'; -import removeBasics from '../../graphql/mutations/event/remove-basics'; -import toggleMainboardSideboardEvent from '../../graphql/mutations/event/toggle-mainboard-sideboard-event'; - -const useStyles = makeStyles({ - iconButton: { - background: theme.palette.secondary.main, - color: backgroundColor, - marginLeft: 8, - marginRight: 8, - '&:hover': { - background: theme.palette.secondary.dark - } - } -}); - -export default function PlaysetDisplay({ - add, - authorizedID, - component, - playset: { card, copies }, - remove, - toggle -}) { - const { eventID, deckID, matchID } = useParams(); - const { userID } = React.useContext(AuthenticationContext); - const classes = useStyles(); - const [updatedCount, setUpdatedCount] = React.useState(copies.length); - - React.useEffect(() => { - setUpdatedCount(copies.length); - }, [copies.length]); - - const isMatch = !!useParams().matchID; - const isEvent = !!useParams().eventID; - - function handleChangeNumberOfCopies() { - if (copies.length < updatedCount) { - if (eventID) { - addBasics({ - headers: { EventID: eventID }, - variables: { - component, - name: card.name, - numberOfCopies: updatedCount - copies.length, - scryfall_id: card.scryfall_id - } - }); - } else { - // TODO don't pass add as props for deck - add(card, component, updatedCount - copies.length); - } - } else if (copies.length > updatedCount) { - if (eventID) { - removeBasics({ - headers: { EventID: eventID }, - variables: { - cardIDs: copies.slice(updatedCount), - component - } - }); - } - // TODO don't pass remove as props for deck - remove(copies.slice(updatedCount), component); - } else { - // don't do anything; no changes - } - } - - return ( -
    - setUpdatedCount(event.target.value)} - style={{ - marginLeft: 16, - marginTop: 4, - width: 64 - }} - type="number" - value={updatedCount} - /> -
    - - { - if (eventID) { - toggleMainboardSideboardEvent({ - headers: { EventID: eventID }, - cardID: copies[0] - }); - } else { - // TODO don't pass toggle as prop - toggle(copies[0]); - } - }} - size="small" - style={{ alignSelf: 'center' }} - > - {useMediaQuery(theme.breakpoints.up('md')) ? ( - - ) : ( - - )} - - - - - {card.name} - - - {card.set.toUpperCase()} - - - -
    -
    - ); -} diff --git a/src/components/miscellaneous/ScryfallCardLink.jsx b/src/components/miscellaneous/ScryfallCardLink.jsx new file mode 100644 index 0000000..990ee94 --- /dev/null +++ b/src/components/miscellaneous/ScryfallCardLink.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import HoverPreview from './HoverPreview'; + +const ScryfallCardLink = ({ card }) => ( + + + {card.name} + + +); + +export default ScryfallCardLink; diff --git a/src/components/miscellaneous/ScryfallRequest.jsx b/src/components/miscellaneous/ScryfallRequest.jsx index ba523a5..cdab76d 100644 --- a/src/components/miscellaneous/ScryfallRequest.jsx +++ b/src/components/miscellaneous/ScryfallRequest.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import MUIAutocomplete from '@mui/material/Autocomplete'; import MUIButton from '@mui/material/Button'; import MUICircularProgress from '@mui/material/CircularProgress'; @@ -9,141 +9,108 @@ import MUIMenu from '@mui/material/Menu'; import MUIMenuItem from '@mui/material/MenuItem'; import MUITextField from '@mui/material/TextField'; -import useRequest from '../../hooks/request-hook'; +import searchCard from '../../graphql/queries/card/search-card'; +import searchPrintings from '../../graphql/queries/card/search-printings'; import HoverPreview from './HoverPreview'; export default function ScryfallRequest({ buttonText, labelText, onSubmit }) { + const abortControllerRef = useRef(new AbortController()); const cardSearchInput = useRef(); - const { loading, sendRequest } = useRequest(); + const timer = useRef(); const [anchorEl, setAnchorEl] = useState(null); const [availablePrintings, setAvailablePrintings] = useState([]); - const [timer, setTimer] = useState(); const [cardSearchResults, setCardSearchResults] = useState([]); - const [chosenCard, setChosenCard] = useState(null); + const [chosenCardOracleID, setChosenCardOracleID] = useState(null); + const [chosenPrinting, setChosenPrinting] = useState(null); + const [loading, setLoading] = useState(false); - const scryfallCardSearch = useCallback( - (event) => { - event.persist(); - setTimer( - setTimeout(async function () { - if (event.target.value.length < 2) { - setCardSearchResults([]); - setChosenCard(null); + const scryfallCardSearch = (event) => { + event.persist(); + if ('current' in timer) { + clearTimeout(timer.current); + } + timer.current = setTimeout(async function () { + try { + if (event.target.value.length < 2) { + setCardSearchResults([]); + setChosenCardOracleID(null); + setChosenPrinting(null); + } else { + setLoading(true); + const cards = await searchCard({ + queryString: `{ + name + oracle_id + }`, + signal: abortControllerRef.current.signal, + variables: { search: event.target.value } + }); + if (cards && cards.data && cards.data.searchCard) { + setCardSearchResults(cards.data.searchCard); } else { - await sendRequest({ - callback: (data) => { - if (data.data) { - setCardSearchResults( - data.data.map((match) => ({ - name: match.name, - oracle_id: match.oracle_id - })) - ); - } else { - setCardSearchResults([]); - } - }, - method: 'GET', - url: `https://api.scryfall.com/cards/search?q=${event.target.value}` - }); + setCardSearchResults([]); } - }, 250) - ); - }, - [sendRequest] - ); + } + } catch (error) { + setCardSearchResults([]); + setChosenCardOracleID(null); + setChosenPrinting(null); + } finally { + setLoading(false); + } + }, 250); + }; - const scryfallPrintSearch = useCallback( - async function (oracleID) { - await sendRequest({ - callback: async (data) => { - const printings = await Promise.all( - data.data.map(async function (print) { - let art_crop, back_image, image; - switch (print.layout) { - case 'adventure': - // this mechanic debuted in Throne of Eldrain. all adventure cards are either (instants or sorceries) and creatures. it seems to have been popular, so it may appear again - art_crop = print.image_uris.art_crop; - image = print.image_uris.large; - break; - case 'flip': - // flip was only in Kamigawa block (plus an "Un" card and a couple of reprints), which was before planeswalkers existed. unlikely they ever bring this layout back, and if they do, no idea how they would fit a planeswalker onto one side. all flip cards are creatures on one end and either a creature or an enchantment on the other - art_crop = print.image_uris.art_crop; - image = print.image_uris.large; - break; - case 'leveler': - // all level up cards have been creatures. this is a mechanic that has so far only appeared in Rise of the Eldrazi and a single card in Modern Horizons. i don't expect the mechanic to return, but the printing of Hexdrinker in MH1 suggests it may - art_crop = print.image_uris.art_crop; - image = print.image_uris.large; - break; - case 'meld': - // meld only appeared in Eldritch Moon and probably won't ever come back. no planeswalkers; only creatures and a single land - art_crop = print.image_uris.art_crop; - const meldResultPart = print.all_parts.find( - (part) => part.component === 'meld_result' - ); - await sendRequest({ - callback: (data) => { - back_image = data.image_uris.large; - image = print.image_uris.large; - }, - method: 'GET', - url: meldResultPart.uri - }); - break; - case 'modal_dfc': - art_crop = print.card_faces[0].image_uris.art_crop; - back_image = print.card_faces[1].image_uris.large; - image = print.card_faces[0].image_uris.large; - break; - case 'saga': - // saga's have no other faces; they simply have their own layout type becuase of the fact that the art is on the right side of the card rather than the top of the card. all sagas printed so far (through Kaldheim) have only 3 or 4 chapters - art_crop = print.image_uris.art_crop; - image = print.image_uris.large; - break; - case 'split': - // split cards are always instants and/or sorceries - art_crop = print.image_uris.art_crop; - image = print.image_uris.large; - break; - case 'transform': - art_crop = print.card_faces[0].image_uris.art_crop; - back_image = print.card_faces[1].image_uris.large; - image = print.card_faces[0].image_uris.large; - break; - default: - art_crop = print.image_uris.art_crop; - image = print.image_uris.large; + useEffect(() => { + (async function () { + try { + if (chosenCardOracleID) { + const printings = await searchPrintings({ + queryString: `{ + _id + card_faces { + image_uris { + art_crop + large + } + } + collector_number + image_uris { + art_crop + large } - return { - art_crop, - back_image, - collector_number: print.collector_number, - image, - name: print.name, - oracle_id: print.oracle_id, - scryfall_id: print.id, - set_name: print.set_name - }; - }) - ); + name + oracle_id + set_name + }`, + signal: abortControllerRef.current.signal, + variables: { oracle_id: chosenCardOracleID } + }); + if (printings && printings.data && printings.data.searchPrintings) { + setAvailablePrintings(printings.data.searchPrintings); + if (printings.data.searchPrintings.length > 0) { + setChosenPrinting(printings[0]); + } + } + } + } catch (error) { + setChosenPrinting(null); + } finally { + } + })(); + }, [chosenCardOracleID]); - setChosenCard(printings[0]); - setAvailablePrintings(printings); - }, - method: 'GET', - url: `https://api.scryfall.com/cards/search?order=released&q=oracleid%3A${oracleID}&unique=prints` - }); - }, - [sendRequest] - ); + useEffect(() => { + return () => abortControllerRef.current.abort(); + }, []); function submitForm() { setAnchorEl(null); setAvailablePrintings([]); setCardSearchResults([]); - onSubmit(chosenCard); - setChosenCard(null); + onSubmit(chosenPrinting); + setChosenCardOracleID(null); + setChosenPrinting(null); cardSearchInput.current.parentElement .getElementsByClassName('MuiAutocomplete-clearIndicator')[0] .click(); @@ -172,7 +139,7 @@ export default function ScryfallRequest({ buttonText, labelText, onSubmit }) { loading={loading} onChange={function (event, value, reason) { if (reason === 'selectOption') { - scryfallPrintSearch(value.oracle_id); + setChosenCardOracleID(value.oracle_id); } }} options={cardSearchResults} @@ -181,23 +148,23 @@ export default function ScryfallRequest({ buttonText, labelText, onSubmit }) { {...params} inputRef={cardSearchInput} label={labelText} - onKeyUp={(event) => { - clearTimeout(timer); - scryfallCardSearch(event); - }} + onChange={scryfallCardSearch} InputProps={{ ...params.InputProps, endAdornment: ( - {loading && ( - - )} + {loading && } {params.InputProps.endAdornment} ) }} /> )} + renderOption={(props, option) => ( +
  • + {option.name} +
  • + )} style={{ marginBottom: 8 }} @@ -221,8 +188,7 @@ export default function ScryfallRequest({ buttonText, labelText, onSubmit }) { @@ -237,18 +203,18 @@ export default function ScryfallRequest({ buttonText, labelText, onSubmit }) { role: 'listbox' }} > - {availablePrintings.map((option, index) => ( + {availablePrintings.map((option) => ( { - setChosenCard({ ...availablePrintings[index] }); + setChosenPrinting(option); setAnchorEl(null); }} - selected={option.scryfall_id === chosenCard.scryfall_id} + selected={option._id === chosenPrinting?._id} > {`${option.set_name} - ${option.collector_number}`} diff --git a/src/components/miscellaneous/VideoAvatar.jsx b/src/components/miscellaneous/VideoAvatar.jsx index f250343..e9d72ff 100644 --- a/src/components/miscellaneous/VideoAvatar.jsx +++ b/src/components/miscellaneous/VideoAvatar.jsx @@ -30,12 +30,7 @@ const useStyles = makeStyles({ } }); -export default function VideoAvatar({ - account, - context, - rtcConnectionIndex, - size -}) { +export default function VideoAvatar({ account, context, rtcConnectionIndex, size }) { const { userID } = useContext(AuthenticationContext); const { setErrorMessages } = useContext(ErrorContext); const { peerConnectionsRef } = useContext(context); @@ -49,12 +44,8 @@ export default function VideoAvatar({ const audioBadge = useRef(); const videoBadge = useRef(); const classes = useStyles(); - const audioTracks = !!mediaStreamRef.current - ? mediaStreamRef.current.getAudioTracks() - : []; - const videoTracks = !!mediaStreamRef.current - ? mediaStreamRef.current.getVideoTracks() - : []; + const audioTracks = !!mediaStreamRef.current ? mediaStreamRef.current.getAudioTracks() : []; + const videoTracks = !!mediaStreamRef.current ? mediaStreamRef.current.getVideoTracks() : []; const pc = peerConnectionsRef.current[rtcConnectionIndex]; async function toggleAudio() { @@ -65,15 +56,9 @@ export default function VideoAvatar({ const microphoneStream = await navigator.mediaDevices.getUserMedia({ audio: true }); - for ( - let index = 0; - index < peerConnectionsRef.current.length; - index++ - ) { + for (let index = 0; index < peerConnectionsRef.current.length; index++) { if (peerConnectionsRef.current[index]) { - audioSendersRef.current[index] = peerConnectionsRef.current[ - index - ].addTrack( + audioSendersRef.current[index] = peerConnectionsRef.current[index].addTrack( microphoneStream.getAudioTracks()[0] // mediaStreamRef.current ); @@ -84,15 +69,9 @@ export default function VideoAvatar({ audioTracks[index].stop(); mediaStreamRef.current.removeTrack(audioTracks[index]); } - for ( - let index = 0; - index < peerConnectionsRef.current.length; - index++ - ) { + for (let index = 0; index < peerConnectionsRef.current.length; index++) { if (peerConnectionsRef.current[index]) { - peerConnectionsRef.current[index].removeTrack( - audioSendersRef.current[index] - ); + peerConnectionsRef.current[index].removeTrack(audioSendersRef.current[index]); audioSendersRef.current[index] = null; } } @@ -105,15 +84,9 @@ export default function VideoAvatar({ audio: true }); // mediaStreamRef.current = microphoneStream; - for ( - let index = 0; - index < peerConnectionsRef.current.length; - index++ - ) { + for (let index = 0; index < peerConnectionsRef.current.length; index++) { if (peerConnectionsRef.current[index]) { - audioSendersRef.current[index] = peerConnectionsRef.current[ - index - ].addTrack( + audioSendersRef.current[index] = peerConnectionsRef.current[index].addTrack( microphoneStream.getAudioTracks()[0] // mediaStreamRef.current ); @@ -137,15 +110,9 @@ export default function VideoAvatar({ const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true }); - for ( - let index = 0; - index < peerConnectionsRef.current.length; - index++ - ) { + for (let index = 0; index < peerConnectionsRef.current.length; index++) { if (peerConnectionsRef.current[index]) { - videoSendersRef.current[index] = peerConnectionsRef.current[ - index - ].addTrack( + videoSendersRef.current[index] = peerConnectionsRef.current[index].addTrack( cameraStream.getVideoTracks()[0], mediaStreamRef.current ); @@ -156,15 +123,9 @@ export default function VideoAvatar({ videoTracks[index].stop(); mediaStreamRef.current.removeTrack(videoTracks[index]); } - for ( - let index = 0; - index < peerConnectionsRef.current.length; - index++ - ) { + for (let index = 0; index < peerConnectionsRef.current.length; index++) { if (peerConnectionsRef.current[index]) { - peerConnectionsRef.current[index].removeTrack( - videoSendersRef.current[index] - ); + peerConnectionsRef.current[index].removeTrack(videoSendersRef.current[index]); videoSendersRef.current[index] = null; } } @@ -177,15 +138,9 @@ export default function VideoAvatar({ video: true }); mediaStreamRef.current = cameraStream; - for ( - let index = 0; - index < peerConnectionsRef.current.length; - index++ - ) { + for (let index = 0; index < peerConnectionsRef.current.length; index++) { if (peerConnectionsRef.current[index]) { - videoSendersRef.current[index] = peerConnectionsRef.current[ - index - ].addTrack( + videoSendersRef.current[index] = peerConnectionsRef.current[index].addTrack( mediaStreamRef.current.getVideoTracks()[0], mediaStreamRef.current ); @@ -235,8 +190,7 @@ export default function VideoAvatar({ return ( ) : ( { - if ( - event.target - .closest('span') - .classList.contains('MuiBadge-colorPrimary') - ) { + if (event.target.closest('span').classList.contains('MuiBadge-colorPrimary')) { toggleVideo(); } }} @@ -370,11 +318,7 @@ export default function VideoAvatar({ color="secondary" onClick={async (event) => { event.persist(); - if ( - event.target - .closest('span') - .classList.contains('MuiBadge-colorSecondary') - ) { + if (event.target.closest('span').classList.contains('MuiBadge-colorSecondary')) { toggleAudio(); } }} @@ -398,8 +342,7 @@ export default function VideoAvatar({ ) : ( { - // don't return anything - }, - logout: () => { - // don't return anything - }, + measurement_system: 'imperial', peerConnection: null, - register: () => { - // don't return anything - }, - requestPasswordReset: () => { + setLoading: () => { // don't return anything }, setLocalStream: () => { @@ -47,18 +30,29 @@ export const AuthenticationContext = createContext({ setUserInfo: () => { // don't return anything }, - submitPasswordReset: () => { - // don't return anything - } + radius: 10, + userID: null, + userName: null }); export function AuthenticationProvider({ children }) { const { setErrorMessages } = useContext(ErrorContext); - const { loading, sendRequest } = useRequest(); + const abortControllerRef = useRef(new AbortController()); + const [loading, setLoading] = useState(false); const [localStream, setLocalStream] = useState(null); const [remoteStreams, setRemoteStreams] = useState([]); const [userInfo, setUserInfo] = useState({ - ...unauthenticatedUserInfo + admin: false, + avatar: { + card_faces: [], + image_uris: {} + }, + buds: [], + conversations: [], + measurement_system: 'imperial', + radius: 10, + userID: null, + userName: null }); const servers = useRef({ iceServers: [ @@ -69,259 +63,82 @@ export function AuthenticationProvider({ children }) { iceCandidatePoolSize: 10 }); const peerConnection = useRef(new RTCPeerConnection(servers.current)); - const authenticationQuery = ` - _id - admin - avatar - name - settings { - measurement_system - radius - } - token - `; - - const storeUserInfo = useCallback(function ({ - _id, - admin, - avatar, - name, - settings, - token - }) { - // store in running application - setUserInfo({ - admin, - avatar, - settings, - userID: _id, - userName: name - }); - // store in browser - Cookies.set('authentication_token', token); - }, - []); - - const authenticate = useCallback( - async function () { - await sendRequest({ - callback: storeUserInfo, - load: true, - operation: 'authenticate', - get body() { - return { - query: ` - query { - ${this.operation} { - ${authenticationQuery} - } - } - ` - }; - } - }); - }, - [sendRequest] - ); - - const login = useCallback( - async function (email, password) { - await sendRequest({ - callback: storeUserInfo, - load: true, - operation: 'login', - get body() { - return { - query: ` - mutation { - ${this.operation}( - email: "${email}", - password: "${password}" - ) { - ${authenticationQuery} - } + useEffect(() => { + (async () => { + try { + if (Cookies.get('authentication_token')) { + setLoading(true); + const { + data: { + authenticate: { + _id, + admin, + avatar, + buds, + conversations, + measurement_system, + name, + radius } - ` - }; - } - }); - }, - [sendRequest] - ); - - const logout = useCallback( - async function () { - // unsubscribe from push notifications if subscribed - let subscription; - - if ('Notification' in window && 'serviceWorker' in navigator) { - const swreg = await navigator.serviceWorker.ready; - subscription = await swreg.pushManager.getSubscription(); - if (subscription) { - try { - await subscription.unsubscribe(); - } catch (error) { - setErrorMessages((prevState) => [...prevState, error.message]); - } - } - } - - // if the logged in user had a push subscription, remove it and the token from the server - await sendRequest({ - operation: 'logoutSingleDevice', - get body() { - return { - query: ` - mutation { - ${this.operation}${ - subscription - ? `( - endpoint: "${subscription.endpoint}" - )` - : '' } - } - ` - }; - } - }); - - // clear from browser and running application - setUserInfo({ - ...unauthenticatedUserInfo - }); - Cookies.remove('authentication_token'); - }, - [sendRequest] - ); - - const register = useCallback( - async function (email, name, password) { - const avatar = { - prints_search_uri: null, - printings: [] - }; - - await sendRequest({ - callback: (data) => { - avatar.prints_search_uri = data.prints_search_uri; - }, - load: true, - method: 'GET', - url: 'https://api.scryfall.com/cards/random' - }); - - await sendRequest({ - callback: (data) => { - avatar.printings = data.data; - }, - load: true, - method: 'GET', - url: avatar.prints_search_uri - }); - - const randomIndex = Math.floor(Math.random() * avatar.printings.length); - - await sendRequest({ - callback: storeUserInfo, - load: true, - operation: 'register', - get body() { - return { - query: ` - mutation { - ${this.operation}( - avatar: "${avatar.printings[randomIndex].image_uris.art_crop}", - email: "${email}", - name: "${name}", - password: "${password}" - ) { - ${authenticationQuery} - } - } - ` - }; - } - }); - }, - [sendRequest] - ); - - const requestPasswordReset = useCallback( - async function (email) { - await sendRequest({ - callback: () => { - setErrorMessages((prevState) => { - return [ - ...prevState, - 'A link to reset your password has been sent. Please check your email inbox and your spam folder.' - ]; + } = await authenticate({ + queryString: authenticateQuery, + signal: abortControllerRef.current.signal + }); + setUserInfo({ + admin, + avatar, + buds, + conversations, + measurement_system, + radius, + userID: _id, + userName: name }); - }, - load: true, - operation: 'requestPasswordReset', - get body() { - return { - query: ` - mutation { - ${this.operation}(email: "${email}") - } - ` - }; } - }); - }, - [sendRequest] - ); + } catch (error) { + Cookies.remove('authentication_token'); + } finally { + setLoading(false); + } + })(); + }, []); - const submitPasswordReset = useCallback( - async function (email, password, reset_token) { - await sendRequest({ - callback: storeUserInfo, - load: true, - operation: 'submitPasswordReset', - get body() { - return { - query: ` - mutation { - ${this.operation}( - email: "${email}" - password: "${password}" - reset_token: "${reset_token}" - ) { - ${authenticationQuery} - } - } - ` - }; - } - }); + useSubscribe({ + cleanup: () => { + abortControllerRef.current.abort(); + abortControllerRef.current = new AbortController(); }, - [sendRequest] - ); - - useEffect(() => { - if (Cookies.get('authentication_token')) { - authenticate(); + condition: !!Cookies.get('authentication_token'), + queryString: authenticateQuery, + subscriptionType: 'subscribeAccount', + update: (data) => { + setUserInfo({ + admin: data.admin, + avatar: data.avatar, + buds: data.buds, + conversations: data.conversations, + measurement_system: data.measurement_system, + radius: data.radius, + userID: data._id, + userName: data.name + }); } - }, []); + }); return ( {children} diff --git a/src/contexts/Permissions.jsx b/src/contexts/Permissions.jsx index 090e319..b94117c 100644 --- a/src/contexts/Permissions.jsx +++ b/src/contexts/Permissions.jsx @@ -1,10 +1,4 @@ -import React, { - createContext, - useContext, - useEffect, - useRef, - useState -} from 'react'; +import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; import urlBase64ToUint8Array from '../functions/url-base64-to-uint8-array'; import useRequest from '../hooks/request-hook'; @@ -71,8 +65,7 @@ export function PermissionsProvider({ children }) { const [microphoneSupported, setMicrophoneSupported] = useState(false); const [notificationsEnabled, setNotificationsEnabled] = useState(false); const [notificationsPermission, setNotificationsPermission] = useState(); - const notificationsSupported = - 'Notification' in window && 'serviceWorker' in navigator; + const notificationsSupported = 'Notification' in window && 'serviceWorker' in navigator; async function turnOnNotificationsAndSubscribeToPushMessaging() { if (isLoggedIn) { @@ -84,19 +77,14 @@ export function PermissionsProvider({ children }) { setNotificationsEnabled(true); const swreg = await navigator.serviceWorker.ready; - const existingSubscription = - await swreg.pushManager.getSubscription(); + const existingSubscription = await swreg.pushManager.getSubscription(); if (!existingSubscription) { const newSubscription = await swreg.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array( - process.env.REACT_APP_VAPID_PUBLIC_KEY - ), + applicationServerKey: urlBase64ToUint8Array(process.env.REACT_APP_VAPID_PUBLIC_KEY), userID }); - const parsedNewSubscription = JSON.parse( - JSON.stringify(newSubscription) - ); + const parsedNewSubscription = JSON.parse(JSON.stringify(newSubscription)); sendRequest({ operation: 'subscribeToPush', get body() { @@ -330,7 +318,10 @@ export function PermissionsProvider({ children }) { // when the app closes, the user logs in/out or the geolocationPermission changes, clear the watch useEffect(() => { - return clearAndDeleteLocation; + return () => { + // opposite of what you might expect due to a closure i suspect... maybe need to use useCallback? + if (!isLoggedIn) clearAndDeleteLocation(); + }; }, [isLoggedIn, geolocationPermission]); useEffect(() => { @@ -340,17 +331,23 @@ export function PermissionsProvider({ children }) { const N = await navigator.permissions.query({ name: 'notifications' }); setNotificationsPermission(N.state); - const devices = - navigator.mediaDevices && - (await navigator.mediaDevices.enumerateDevices()); + const devices = navigator.mediaDevices && (await navigator.mediaDevices.enumerateDevices()); - if (devices.some((device) => device.kind === 'videoinput')) { + if ( + devices.some((device) => device.kind === 'videoinput') && + navigator.permissions && + 'camera' in navigator.permissions + ) { setCameraSupported(true); const C = await navigator.permissions.query({ name: 'camera' }); setCameraPermission(C.state); } - if (devices.some((device) => device.kind === 'audioinput')) { + if ( + devices.some((device) => device.kind === 'audioinput') && + navigator.permissions && + 'microphone' in navigator.permissions + ) { setMicrophoneSupported(true); const M = await navigator.permissions.query({ name: 'microphone' }); setMicrophonePermission(M.state); diff --git a/src/contexts/account-context.jsx b/src/contexts/account-context.jsx index 26774a8..bc8a417 100644 --- a/src/contexts/account-context.jsx +++ b/src/contexts/account-context.jsx @@ -1,25 +1,24 @@ -import React, { - createContext, - useCallback, - useContext, - useEffect, - useState -} from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import React, { createContext, /* useCallback, */ useContext, useRef, useState } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import accountQuery from '../constants/account-query'; import fetchAccountByID from '../graphql/queries/account/fetch-account-by-ID'; -import useRequest from '../hooks/request-hook'; +// import useRequest from '../hooks/request-hook'; +import useSubscribe from '../hooks/subscribe-hook'; import Account from '../pages/Account'; import LoadingSpinner from '../components/miscellaneous/LoadingSpinner'; -import { AuthenticationContext } from './Authentication'; -import { CardCacheContext } from './CardCache'; import { ErrorContext } from './Error'; export const AccountContext = createContext({ - loading: false, + abortControllerRef: { current: new AbortController() }, accountState: { _id: '', - avatar: '', + avatar: { + card_faces: null, + image_uris: { + art_crop: '...' + } + }, buds: [], cubes: [], decks: [], @@ -35,23 +34,26 @@ export const AccountContext = createContext({ sent_bud_requests: [], total_events: 0 }, - setAccountState: () => null, - createMatch: () => null, + setAccountState: () => null + // createMatch: () => null // deleteEvent: () => null, - // deleteMatch: () => null, - editAccount: () => null + // deleteMatch: () => null }); export default function ContextualizedAccountPage() { - const { setUserInfo, userID } = useContext(AuthenticationContext); - const { addCardsToCache, scryfallCardDataCache } = - useContext(CardCacheContext); const { setErrorMessages } = useContext(ErrorContext); - const navigate = useNavigate(); + const location = useLocation(); + // const navigate = useNavigate(); const { accountID } = useParams(); + const abortControllerRef = useRef(new AbortController()); const [accountState, setAccountState] = useState({ _id: accountID, - avatar: '', + avatar: { + card_faces: null, + image_uris: { + art_crop: '...' + } + }, buds: [], cubes: [], decks: [], @@ -68,272 +70,77 @@ export default function ContextualizedAccountPage() { total_events: 0 }); const [loading, setLoading] = useState(false); - const accountQuery = ` - _id - avatar - buds { - _id - avatar - buds { - _id - avatar - name - } - decks { - _id - format - name - } - name - } - cubes { - _id - description - image - mainboard { - _id - } - modules { - _id - cards { - _id - } - name - } - name - rotations { - _id - cards { - _id - } - name - size - } - sideboard { - _id - } - } - decks { - _id - description - format - image - name - } - email - events { - _id - createdAt - cube { - _id - image - name - } - host { - _id - avatar - name - } - name - players { - account { - _id - avatar - name - } - } - } - location { - coordinates - } - matches { - _id - createdAt - cube { - _id - name - } - decks { - _id - format - name - } - event { - _id - name - } - players { - account { - _id - avatar - name - } - } - } - name - nearby_users { - _id - avatar - name - } - received_bud_requests { - _id - avatar - name - } - sent_bud_requests { - _id - avatar - name - } - settings { - measurement_system - radius - } - total_events - `; - const { sendRequest } = useRequest(); - - const updateAccountState = useCallback( - async function (data) { - const cardSet = new Set(); - - for (const cube of data.cubes) { - if (cube.image) cardSet.add(cube.image); - } - - for (const deck of data.decks) { - if (deck.image) cardSet.add(deck.image); - } - - for (const event of data.events) { - if (event.cube.image) cardSet.add(event.cube.image); - } - - await addCardsToCache([...cardSet]); - - for (const cube of data.cubes) { - if (cube.image) { - cube.image = { - alt: scryfallCardDataCache.current[cube.image].name, - scryfall_id: cube.image, - src: scryfallCardDataCache.current[cube.image].art_crop - }; - } - } - - for (const deck of data.decks) { - if (deck.image) { - deck.image = { - alt: scryfallCardDataCache.current[deck.image].name, - scryfall_id: deck.image, - src: scryfallCardDataCache.current[deck.image].art_crop - }; - } - } - - for (const event of data.events) { - if (event.cube.image) { - event.cube.image = { - alt: scryfallCardDataCache.current[event.cube.image].name, - scryfall_id: event.cube.image, - src: scryfallCardDataCache.current[event.cube.image].art_crop - }; - } - } - - setAccountState(data); + const { accountData } = location.state || {}; + // const { sendRequest } = useRequest(); + + // const createMatch = useCallback( + // async function (event, deckIDs, eventID, playerIDs) { + // event.preventDefault(); + + // await sendRequest({ + // callback: (data) => { + // navigate(`/match/${data._id}`); + // }, + // load: true, + // operation: 'createMatch', + // get body() { + // return { + // query: ` + // mutation { + // ${this.operation}( + // deckIDs: [${deckIDs.map((dckID) => '"' + dckID + '"')}], + // ${eventID ? 'eventID: "' + eventID + '",\n' : ''} + // playerIDs: [${playerIDs.map((plrID) => '"' + plrID + '"')}] + // ) { + // _id + // } + // } + // ` + // }; + // } + // }); + // }, + // [navigate, sendRequest] + // ); + + useSubscribe({ + cleanup: () => { + abortControllerRef.current.abort(); + abortControllerRef.current = new AbortController(); }, - [addCardsToCache] - ); - - const createMatch = useCallback( - async function (event, deckIDs, eventID, playerIDs) { - event.preventDefault(); - - await sendRequest({ - callback: (data) => { - navigate(`/match/${data._id}`); - }, - load: true, - operation: 'createMatch', - get body() { - return { - query: ` - mutation { - ${this.operation}( - deckIDs: [${deckIDs.map((dckID) => '"' + dckID + '"')}], - ${eventID ? 'eventID: "' + eventID + '",\n' : ''} - playerIDs: [${playerIDs.map((plrID) => '"' + plrID + '"')}] - ) { - _id - } - } - ` - }; - } - }); - }, - [navigate, sendRequest] - ); - - const editAccount = useCallback( - async function (changes) { - await sendRequest({ - callback: (data) => { - setAccountState(data); - if (!changes.toString().includes('return_other')) { - setUserInfo((prevState) => ({ - ...prevState, - avatar: data.avatar, - settings: data.settings, - userName: data.name - })); - } - }, - operation: 'editAccount', - get body() { - return { - query: ` - mutation { - ${this.operation}( - ${changes} - ) { - ${accountQuery} - } - } - ` - }; + connectionInfo: { accountID }, + dependencies: [accountID], + queryString: accountQuery, + setup: async () => { + if (accountData) { + setAccountState(accountData); + } else { + try { + setLoading(true); + const response = await fetchAccountByID({ + headers: { AccountID: accountID }, + queryString: accountQuery, + signal: abortControllerRef.current.signal + }); + setAccountState(response.data.fetchAccountByID); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setLoading(false); } - }); - }, - [accountQuery, sendRequest] - ); - - useEffect(() => { - (async function () { - try { - setLoading(true); - const data = await fetchAccountByID({ - headers: { AccountID: accountID }, - queryString: `{\n${accountQuery}\n}` - }); - await updateAccountState(data.data.fetchAccountByID); - } catch (error) { - setErrorMessages((prevState) => [...prevState, error.message]); - } finally { - setLoading(false); } - })(); - }, [accountID, userID]); + }, + subscriptionType: 'subscribeAccount', + update: setAccountState + }); return ( {loading ? : } diff --git a/src/contexts/blog-post-context.jsx b/src/contexts/blog-post-context.jsx index 5032cca..07b60b3 100644 --- a/src/contexts/blog-post-context.jsx +++ b/src/contexts/blog-post-context.jsx @@ -1,24 +1,25 @@ -import React, { - createContext, - useCallback, - useContext, - useEffect, - useState -} from 'react'; +import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; -import useRequest from '../hooks/request-hook'; +import blogPostQuery from '../constants/blog-post-query'; +import fetchBlogPostByID from '../graphql/queries/blog/fetch-blog-post-by-ID'; import useSubscribe from '../hooks/subscribe-hook'; import BlogPost from '../pages/BlogPost'; +import LoadingSpinner from '../components/miscellaneous/LoadingSpinner'; import { AuthenticationContext } from './Authentication'; +import { ErrorContext } from './Error'; export const BlogPostContext = createContext({ + abortControllerRef: { current: new AbortController() }, loading: false, blogPostState: { _id: '', author: { _id: '', - avatar: '', + avatar: { + card_faces: [], + image_uris: null + }, name: '' }, body: '', @@ -30,18 +31,22 @@ export const BlogPostContext = createContext({ createdAt: 0, updatedAt: 0 }, - createComment: () => null, setBlogPostState: () => null }); export default function ContextualizedBlogPostPage() { - const { blogPostID } = useParams(); const { avatar, userID, userName } = useContext(AuthenticationContext); + const { setErrorMessages } = useContext(ErrorContext); + const { blogPostID } = useParams(); + const abortControllerRef = useRef(new AbortController()); const [blogPostState, setBlogPostState] = useState({ _id: null, author: { _id: '', - avatar: '', + avatar: { + card_faces: [], + image_uris: null + }, name: '...' }, body: '', @@ -53,83 +58,32 @@ export default function ContextualizedBlogPostPage() { createdAt: null, updatedAt: null }); - const blogPostQuery = ` - _id - author { - _id - avatar - name - } - body - comments { - _id - author { - _id - avatar - name - } - body - createdAt - updatedAt - } - image - published - subtitle - title - createdAt - updatedAt - `; - const { loading, sendRequest } = useRequest(); - - const createComment = useCallback( - async function (newComment) { - await sendRequest({ - headers: { BlogPostID: blogPostID }, - operation: 'createComment', - get body() { - return { - query: ` - mutation { - ${this.operation}(body: "${newComment}") { - _id - } - } - ` - }; - } - }); - }, - [sendRequest] - ); - - const fetchBlogPostByID = useCallback( - async function () { - await sendRequest({ - callback: setBlogPostState, - headers: { BlogPostID: blogPostID }, - load: true, - operation: 'fetchBlogPostByID', - get body() { - return { - query: ` - query { - ${this.operation} { - ${blogPostQuery} - } - } - ` - }; - } - }); - }, - [blogPostQuery, blogPostID, sendRequest] - ); + const [loading, setLoading] = useState(false); if (blogPostID !== 'new-post') { useSubscribe({ + cleanup: () => { + abortControllerRef.current.abort(); + abortControllerRef.current = new AbortController(); + }, connectionInfo: { blogPostID }, + dependencies: [blogPostID], queryString: blogPostQuery, - setup: fetchBlogPostByID, + setup: async () => { + try { + setLoading(true); + const response = await fetchBlogPostByID({ + headers: { BlogPostID: blogPostID }, + queryString: blogPostQuery, + signal: abortControllerRef.current.signal + }); + setBlogPostState(response.data.fetchBlogPostByID); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setLoading(false); + } + }, subscriptionType: 'subscribeBlogPost', update: setBlogPostState }); @@ -154,13 +108,12 @@ export default function ContextualizedBlogPostPage() { return ( - + {loading ? : } ); } diff --git a/src/contexts/cube-context.jsx b/src/contexts/cube-context.jsx index 14b19e0..0e0ce59 100644 --- a/src/contexts/cube-context.jsx +++ b/src/contexts/cube-context.jsx @@ -1,20 +1,16 @@ -import React, { - createContext, - useCallback, - useContext, - useEffect, - useState -} from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; -import usePopulate from '../hooks/populate-hook'; +import cubeQuery from '../constants/cube-query'; +import fetchCubeByID from '../graphql/queries/cube/fetch-cube-by-ID'; import useRequest from '../hooks/request-hook'; import useSubscribe from '../hooks/subscribe-hook'; import Cube from '../pages/Cube'; import LoadingSpinner from '../components/miscellaneous/LoadingSpinner'; -import { CardCacheContext } from './CardCache'; +import { ErrorContext } from './Error'; export const CubeContext = createContext({ + abortControllerRef: { current: new AbortController() }, activeComponentState: { _id: 'mainbaord', displayedCards: [], @@ -26,14 +22,27 @@ export const CubeContext = createContext({ _id: null, creator: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: null }, description: null, image: { - alt: undefined, - scryfall_id: undefined, - src: undefined + _id: '', + image_uris: { + art_crop: '' + }, + name: '', + card_faces: [ + { + image_uris: { + art_crop: '' + }, + name: '' + } + ] }, mainboard: [], modules: [], @@ -47,21 +56,20 @@ export const CubeContext = createContext({ filter: '' }, setDisplayState: () => null, - // addCardToCube: () => null, - cloneCube: () => null, createModule: () => null, createRotation: () => null, deleteCard: () => null, deleteModule: () => null, deleteRotation: () => null, - editCard: () => null, - editCube: () => null, - editModule: () => null, editRotation: () => null }); export default function ContextualizedCubePage() { - const navigate = useNavigate(); + const { setErrorMessages } = useContext(ErrorContext); + const location = useLocation(); + const { cubeID } = useParams(); + const abortControllerRef = useRef(new AbortController()); + const { sendRequest } = useRequest(); const [activeComponentState, setActiveComponentState] = useState({ _id: 'mainboard', displayedCards: [], @@ -69,21 +77,31 @@ export default function ContextualizedCubePage() { name: 'Mainboard', size: null }); - const { cubeID } = useParams(); - const { addCardsToCache, scryfallCardDataCache } = - useContext(CardCacheContext); const [cubeState, setCubeState] = useState({ _id: cubeID, creator: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: '...' }, description: '', image: { - alt: undefined, - scryfall_id: undefined, - src: undefined + _id: '', + image_uris: { + art_crop: '' + }, + name: '', + card_faces: [ + { + image_uris: { + art_crop: '' + }, + name: '' + } + ] }, mainboard: [], modules: [], @@ -96,49 +114,8 @@ export default function ContextualizedCubePage() { activeComponentID: 'mainboard', filter: '' }); - const cardQuery = ` - _id - cmc - color_identity - notes - scryfall_id - type_line - `; - const cubeQuery = ` - _id - creator { - _id - avatar - name - } - description - image - mainboard { - ${cardQuery} - } - modules { - _id - cards { - ${cardQuery} - } - name - } - name - published - rotations { - _id - cards { - ${cardQuery} - } - name - size - } - sideboard { - ${cardQuery} - } - `; - const { populateCachedScryfallData } = usePopulate(); - const { loading, sendRequest } = useRequest(); + const [loading, setLoading] = useState(false); + const { cubeData } = location.state || {}; const filterCards = useCallback( (cards, text) => @@ -146,12 +123,9 @@ export default function ContextualizedCubePage() { const wordArray = text.split(' '); return wordArray.every( (word) => - card.keywords - .join(' ') - .toLowerCase() - .includes(word.toLowerCase()) || - card.name.toLowerCase().includes(word.toLowerCase()) || - card.type_line.toLowerCase().includes(word.toLowerCase()) + card.scryfall_card.keywords.join(' ').toLowerCase().includes(word.toLowerCase()) || + card.scryfall_card.name.toLowerCase().includes(word.toLowerCase()) || + card.scryfall_card.type_line.toLowerCase().includes(word.toLowerCase()) ); }), [] @@ -161,18 +135,13 @@ export default function ContextualizedCubePage() { const state = { _id: displayState.activeComponentID }; if (state._id === 'sideboard') { - state.displayedCards = filterCards( - cubeState.sideboard, - displayState.filter - ); + state.displayedCards = filterCards(cubeState.sideboard, displayState.filter); state.name = 'Sideboard'; } else if (cubeState.modules.find((module) => module._id === state._id)) { const module = cubeState.modules.find((mdl) => mdl._id === state._id); state.displayedCards = filterCards(module.cards, displayState.filter); state.name = module.name; - } else if ( - cubeState.rotations.find((rotation) => rotation._id === state._id) - ) { + } else if (cubeState.rotations.find((rotation) => rotation._id === state._id)) { const rotation = cubeState.rotations.find((rtn) => rtn._id === state._id); state.displayedCards = filterCards(rotation.cards, displayState.filter); state.maxSize = rotation.cards.length; @@ -180,119 +149,13 @@ export default function ContextualizedCubePage() { state.size = rotation.size; } else { state._id = 'mainboard'; - state.displayedCards = filterCards( - cubeState.mainboard, - displayState.filter - ); + state.displayedCards = filterCards(cubeState.mainboard, displayState.filter); state.name = 'Mainboard'; } setActiveComponentState(state); }, [cubeState, displayState, filterCards]); - const updateCubeState = useCallback( - async function (data) { - const cardSet = new Set(); - - if (data.image) cardSet.add(data.image); - - for (const card of data.mainboard) { - cardSet.add(card.scryfall_id); - } - - for (const card of data.sideboard) { - cardSet.add(card.scryfall_id); - } - - for (const module of data.modules) { - for (const card of module.cards) { - cardSet.add(card.scryfall_id); - } - } - - for (const rotation of data.rotations) { - for (const card of rotation.cards) { - cardSet.add(card.scryfall_id); - } - } - - await addCardsToCache([...cardSet]); - - if (data.image) { - data.image = { - alt: scryfallCardDataCache.current[data.image].name, - scryfall_id: data.image, - src: scryfallCardDataCache.current[data.image].art_crop - }; - } - - data.mainboard.forEach(populateCachedScryfallData); - - data.sideboard.forEach(populateCachedScryfallData); - - for (const module of data.modules) { - module.cards.forEach(populateCachedScryfallData); - } - - for (const rotation of data.rotations) { - rotation.cards.forEach(populateCachedScryfallData); - } - - setCubeState(data); - }, - [addCardsToCache, populateCachedScryfallData] - ); - - // const addCardToCube = useCallback( - // async function ({ name, scryfall_id }) { - // await sendRequest({ - // headers: { CubeID: cubeState._id }, - // operation: 'addCardToCube', - // get body() { - // return { - // query: ` - // mutation { - // ${this.operation}( - // componentID: "${activeComponentState._id}", - // name: "${name}", - // scryfall_id: "${scryfall_id}" - // ) { - // _id - // } - // } - // ` - // }; - // } - // }); - // }, - // [activeComponentState._id, cubeState._id, sendRequest] - // ); - - const cloneCube = useCallback( - async function () { - await sendRequest({ - callback: (data) => { - navigate(`/cube/${data._id}`); - }, - headers: { CubeID: cubeState._id }, - load: true, - operation: 'cloneCube', - get body() { - return { - query: ` - mutation { - ${this.operation} { - ${cubeQuery} - } - } - ` - }; - } - }); - }, - [cubeQuery, cubeState._id, navigate, sendRequest] - ); - const createModule = useCallback( async function (name, toggleOpen) { await sendRequest({ @@ -367,9 +230,7 @@ export default function ContextualizedCubePage() { mutation { ${this.operation}( cardID: "${cardID}", - ${ - destinationID ? 'destinationID: "' + destinationID + '",' : '' - } + ${destinationID ? 'destinationID: "' + destinationID + '",' : ''} originID: "${activeComponentState._id}" ) } @@ -431,80 +292,6 @@ export default function ContextualizedCubePage() { [activeComponentState._id, cubeState._id, sendRequest] ); - const editCard = useCallback( - async function (changes) { - await sendRequest({ - headers: { CubeID: cubeState._id }, - operation: 'editCard', - get body() { - return { - query: ` - mutation { - ${this.operation}( - componentID: "${activeComponentState._id}", - ${changes} - ) { - _id - } - } - ` - }; - } - }); - }, - [activeComponentState._id, cubeState._id, sendRequest] - ); - - const editCube = useCallback( - async function (description, image, name, published) { - await sendRequest({ - headers: { CubeID: cubeState._id }, - operation: 'editCube', - get body() { - return { - query: ` - mutation { - ${this.operation}( - description: "${description}", - ${image ? `image: "${image}",` : ''} - name: "${name}", - published: ${published} - ) { - _id - } - } - ` - }; - } - }); - }, - [cubeState._id, sendRequest] - ); - - const editModule = useCallback( - async function (name) { - await sendRequest({ - headers: { CubeID: cubeState._id }, - operation: 'editModule', - get body() { - return { - query: ` - mutation { - ${this.operation}( - moduleID: "${activeComponentState._id}", - name: "${name}" - ) { - _id - } - } - ` - }; - } - }); - }, - [activeComponentState._id, cubeState._id, sendRequest] - ); - const editRotation = useCallback( async function (name, size) { await sendRequest({ @@ -530,54 +317,50 @@ export default function ContextualizedCubePage() { [activeComponentState._id, cubeState._id, sendRequest] ); - const fetchCubeByID = useCallback( - async function () { - await sendRequest({ - callback: updateCubeState, - headers: { CubeID: cubeState._id }, - load: true, - operation: 'fetchCubeByID', - get body() { - return { - query: ` - query { - ${this.operation} { - ${cubeQuery} - } - } - ` - }; - } - }); - }, - [cubeQuery, cubeState._id, sendRequest, updateCubeState] - ); - useSubscribe({ + cleanup: () => { + abortControllerRef.current.abort(); + abortControllerRef.current = new AbortController(); + }, connectionInfo: { cubeID }, + dependencies: [cubeID], queryString: cubeQuery, - setup: fetchCubeByID, + setup: async () => { + if (cubeData) { + setCubeState(cubeData); + } else { + try { + setLoading(true); + const response = await fetchCubeByID({ + headers: { CubeID: cubeID }, + queryString: cubeQuery, + signal: abortControllerRef.current.signal + }); + setCubeState(response.data.fetchCubeByID); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setLoading(false); + } + } + }, subscriptionType: 'subscribeCube', - update: updateCubeState + update: setCubeState }); return ( diff --git a/src/contexts/deck-context.jsx b/src/contexts/deck-context.jsx index aec4d91..ddc60db 100644 --- a/src/contexts/deck-context.jsx +++ b/src/contexts/deck-context.jsx @@ -1,290 +1,146 @@ -import React, { createContext, useCallback, useContext, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; - -import usePopulate from '../hooks/populate-hook'; -import useRequest from '../hooks/request-hook'; +import React, { + createContext, + useContext, + /* useEffect, */ + useRef, + useState +} from 'react'; +import { useLocation, useParams } from 'react-router-dom'; + +import deckQuery from '../constants/deck-query'; +import fetchDeckByID from '../graphql/queries/deck/fetch-deck-by-ID'; import useSubscribe from '../hooks/subscribe-hook'; +// import validateDeck from '../functions/validate-deck'; import Deck from '../pages/Deck'; -import { CardCacheContext } from './CardCache'; +import LoadingSpinner from '../components/miscellaneous/LoadingSpinner'; +import { ErrorContext } from './Error'; export const DeckContext = createContext({ - loading: false, + abortControllerRef: { current: new AbortController() }, deckState: { _id: '', + cards: [], creator: { _id: '', - avatar: '', + avatar: { + card_faces: [], + image_uris: null + }, name: '' }, description: '', format: '', image: { - alt: undefined, - scryfall_id: undefined, - src: undefined + _id: '', + image_uris: { + art_crop: '' + }, + name: '', + card_faces: [ + { + image_uris: { + art_crop: '' + }, + name: '' + } + ] }, - mainboard: [], name: '', - published: false, - sideboard: [] - }, - addCardsToDeck: () => null, - cloneDeck: () => null, - editDeck: () => null, - removeCardsFromDeck: () => null, - toggleMainboardSideboardDeck: () => null + published: false + } + // warnings: [] }); export default function ContextualizedDeckPage() { - const navigate = useNavigate(); + const { setErrorMessages } = useContext(ErrorContext); + const location = useLocation(); const { deckID } = useParams(); - const { addCardsToCache, scryfallCardDataCache } = - useContext(CardCacheContext); + const abortControllerRef = useRef(new AbortController()); + const [loading, setLoading] = useState(false); const [deckState, setDeckState] = useState({ _id: deckID, + cards: [], creator: { _id: '', - avatar: '', + avatar: { + card_faces: [], + image_uris: null + }, name: '...' }, description: '', format: '', image: { - alt: undefined, - scryfall_id: undefined, - src: undefined - }, - mainboard: [], - name: '', - published: false, - sideboard: [] - }); - const cardQuery = ` - _id - scryfall_id - `; - const deckQuery = ` - _id - creator { - _id - avatar - name - } - description - format - image - mainboard { - ${cardQuery} - } - name - published - sideboard { - ${cardQuery} - } - `; - const { loading, sendRequest } = useRequest(); - const { populateCachedScryfallData } = usePopulate(); - - const updateDeckState = useCallback( - async function (data) { - const cardSet = new Set(); - - if (data.image) cardSet.add(data.image); - - for (const card of data.mainboard) { - cardSet.add(card.scryfall_id); - } - - for (const card of data.sideboard) { - cardSet.add(card.scryfall_id); - } - - await addCardsToCache([...cardSet]); - - if (data.image) { - data.image = { - alt: scryfallCardDataCache.current[data.image].name, - scryfall_id: data.image, - src: scryfallCardDataCache.current[data.image].art_crop - }; - } - - data.mainboard.forEach(populateCachedScryfallData); - - data.sideboard.forEach(populateCachedScryfallData); - - setDeckState(data); - }, - [addCardsToCache, populateCachedScryfallData] - ); - - const addCardsToDeck = useCallback( - async function ({ name, scryfall_id }, component, numberOfCopies) { - await sendRequest({ - headers: { DeckID: deckState._id }, - operation: 'addCardsToDeck', - get body() { - return { - query: ` - mutation { - ${this.operation}( - component: ${component}, - name: "${name}", - numberOfCopies: ${numberOfCopies}, - scryfall_id: "${scryfall_id}" - ) { - _id - } - } - ` - }; - } - }); - }, - [deckState._id, sendRequest] - ); - - const cloneDeck = useCallback( - async function () { - await sendRequest({ - callback: (data) => { - navigate(`/deck/${data._id}`); + _id: '', + image_uris: { + image_uris: { + art_crop: '' }, - headers: { DeckID: deckState._id }, - load: true, - operation: 'cloneDeck', - get body() { - return { - query: ` - mutation { - ${this.operation} { - ${deckQuery} - } - } - ` - }; - } - }); - }, - [deckQuery, deckState._id, navigate, sendRequest] - ); - - const editDeck = useCallback( - async function ({ description, format, image, name, published }) { - await sendRequest({ - headers: { DeckID: deckState._id }, - operation: 'editDeck', - get body() { - return { - query: ` - mutation { - ${this.operation}( - description: "${description}", - format: ${format}, - ${image ? `image: "${image}",` : ''} - published: ${published}, - name: "${name}" - ) { - _id - } - } - ` - }; - } - }); - }, - [deckState._id, sendRequest] - ); - - const fetchDeckByID = useCallback( - async function () { - await sendRequest({ - callback: updateDeckState, - headers: { DeckID: deckState._id }, - load: true, - operation: 'fetchDeckByID', - get body() { - return { - query: ` - query { - ${this.operation} { - ${deckQuery} - } - } - ` - }; - } - }); - }, - [deckQuery, deckState._id, sendRequest, updateDeckState] - ); - - const removeCardsFromDeck = useCallback( - async function (cardIDs, component) { - await sendRequest({ - headers: { DeckID: deckState._id }, - operation: 'removeCardsFromDeck', - get body() { - return { - query: ` - mutation { - ${this.operation}( - cardIDs: [${cardIDs.map((cardID) => '"' + cardID + '"')}], - component: ${component} - ) { - _id - } - } - ` - }; + name: '' + }, + name: '', + card_faces: [ + { + image_uris: { + art_crop: '' + }, + name: '' } - }); + ] }, - [deckState._id, sendRequest] - ); + name: '', + published: false + }); + // const [warnings, setWarnings] = useState([]); - const toggleMainboardSideboardDeck = useCallback( - async function (cardID) { - await sendRequest({ - headers: { DeckID: deckState._id }, - operation: 'toggleMainboardSideboardDeck', - get body() { - return { - query: ` - mutation { - ${this.operation}(cardID: "${cardID}") { - _id - } - } - ` - }; - } - }); - }, - [deckState._id, sendRequest] - ); + const { deckData } = location.state || {}; useSubscribe({ + cleanup: () => { + abortControllerRef.current.abort(); + abortControllerRef.current = new AbortController(); + }, connectionInfo: { deckID }, + dependencies: [deckID], queryString: deckQuery, - setup: fetchDeckByID, + setup: async () => { + if (deckData) { + setDeckState(deckData); + } else { + try { + setLoading(true); + const response = await fetchDeckByID({ + headers: { DeckID: deckID }, + queryString: deckQuery, + signal: abortControllerRef.current.signal + }); + setDeckState(response.data.fetchDeckByID); + } catch (error) { + setErrorMessages((prevState) => [...prevState, error.message]); + } finally { + setLoading(false); + } + } + }, + fetchDeckByID, subscriptionType: 'subscribeDeck', - update: updateDeckState + update: setDeckState }); + // useEffect(() => { + // const { format, mainboard, sideboard } = deckState; + // validateDeck({ format, mainboard, sideboard }, setWarnings); + // }, [deckState.format, deckState.mainboard.length, deckState.sideboard.length]); + return ( - + {loading ? : } ); } diff --git a/src/contexts/event-context.jsx b/src/contexts/event-context.jsx index 7669ea8..524aef7 100644 --- a/src/contexts/event-context.jsx +++ b/src/contexts/event-context.jsx @@ -21,7 +21,10 @@ export const EventContext = createContext({ finished: false, host: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: '...' }, name: null, @@ -29,7 +32,10 @@ export const EventContext = createContext({ { account: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: '...' }, answers: [], @@ -45,7 +51,10 @@ export const EventContext = createContext({ me: { account: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: '...' }, answers: [], @@ -72,7 +81,10 @@ export default function ContextualizedEventPage() { finished: false, host: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: '...' }, name: null, @@ -90,7 +102,16 @@ export default function ContextualizedEventPage() { _id author { _id - avatar + avatar { + card_faces { + image_uris { + art_crop + } + } + image_uris { + art_crop + } + } name } body @@ -104,7 +125,16 @@ export default function ContextualizedEventPage() { players { account { _id - avatar + avatar { + card_faces { + image_uris { + art_crop + } + } + image_uris { + art_crop + } + } name } current_pack { @@ -165,10 +195,7 @@ export default function ContextualizedEventPage() { const playerIndex = eventState.players.findIndex( (player) => player.account._id === data.remote_account._id ); - if ( - Number.isInteger(playerIndex) && - !!peerConnectionsRef.current[playerIndex] - ) { + if (Number.isInteger(playerIndex) && !!peerConnectionsRef.current[playerIndex]) { switch (data.__typename) { case 'ICECandidate': const { candidate, sdpMLineIndex, sdpMid, usernameFragment } = data; @@ -183,15 +210,9 @@ export default function ContextualizedEventPage() { const { sdp, type } = data; switch (type) { case 'offer': - await peerConnectionsRef.current[ - playerIndex - ].setRemoteDescription({ type, sdp }); - const answer = await peerConnectionsRef.current[ - playerIndex - ].createAnswer(); - await peerConnectionsRef.current[playerIndex].setLocalDescription( - answer - ); + await peerConnectionsRef.current[playerIndex].setRemoteDescription({ type, sdp }); + const answer = await peerConnectionsRef.current[playerIndex].createAnswer(); + await peerConnectionsRef.current[playerIndex].setLocalDescription(answer); sendRTCSessionDescription({ variables: { accountIDs: [eventState.players[playerIndex].account._id], @@ -279,9 +300,10 @@ export default function ContextualizedEventPage() { useSubscribe({ cleanup: () => { abortControllerRef.current.abort(); + abortControllerRef.current = new AbortController(); }, connectionInfo: { eventID }, - dependencies: [eventID, userID], + dependencies: [eventID], queryString: eventQuery, setup: async () => { try { @@ -339,9 +361,7 @@ export default function ContextualizedEventPage() { peerConnectionsRef.current = []; for (let index = 0; index < eventState.players.length; index++) { if (eventState.players[index].account._id !== userID) { - const newPeerConnection = new RTCPeerConnection( - RTCPeerConnectionConfig - ); + const newPeerConnection = new RTCPeerConnection(RTCPeerConnectionConfig); newPeerConnection.onicecandidate = onIceCandidate; newPeerConnection.onnegotiationneeded = onNegotiationNeeded; diff --git a/src/contexts/match-context.jsx b/src/contexts/match-context.jsx index b383de4..3d36d1d 100644 --- a/src/contexts/match-context.jsx +++ b/src/contexts/match-context.jsx @@ -11,7 +11,10 @@ export const MatchContext = createContext({ bottomPlayerState: { account: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: null }, battlefield: [], @@ -34,7 +37,10 @@ export const MatchContext = createContext({ { account: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: null }, battlefield: [], @@ -52,7 +58,10 @@ export const MatchContext = createContext({ { account: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: null }, battlefield: [], @@ -80,7 +89,10 @@ export const MatchContext = createContext({ topPlayerState: { account: { _id: null, - avatar: null, + avatar: { + card_faces: [], + image_uris: null + }, name: null }, battlefield: [], @@ -136,7 +148,10 @@ export default function ContextualizedMatchPage() { { account: { _id: 'A', - avatar: '', + avatar: { + card_faces: [], + image_uris: null + }, name: '...' }, battlefield: [], @@ -154,7 +169,10 @@ export default function ContextualizedMatchPage() { { account: { _id: 'B', - avatar: '', + avatar: { + card_faces: [], + image_uris: null + }, name: '...' }, battlefield: [], @@ -179,24 +197,38 @@ export default function ContextualizedMatchPage() { title: null, updateFunction: null }); - const [bottomPlayerState, setBottomPlayerState] = React.useState( - matchState.players[0] - ); - const [topPlayerState, setTopPlayerState] = React.useState( - matchState.players[1] - ); + const [bottomPlayerState, setBottomPlayerState] = React.useState(matchState.players[0]); + const [topPlayerState, setTopPlayerState] = React.useState(matchState.players[1]); const matchQuery = ` _id game_winners { _id - avatar + avatar { + card_faces { + image_uris { + art_crop + } + } + image_uris { + art_crop + } + } name } log players { account { _id - avatar + avatar { + card_faces { + image_uris { + art_crop + } + } + image_uris { + art_crop + } + } name } battlefield { @@ -397,20 +429,14 @@ export default function ContextualizedMatchPage() { React.useEffect(() => { // this allows a more smooth drag and drop experience - const me = matchState.players.find( - (player) => player.account._id === userID - ); + const me = matchState.players.find((player) => player.account._id === userID); if (me) { - if (JSON.stringify(bottomPlayerState) !== JSON.stringify(me)) - setBottomPlayerState(me); + if (JSON.stringify(bottomPlayerState) !== JSON.stringify(me)) setBottomPlayerState(me); - const opponent = matchState.players.find( - (player) => player.account._id !== userID - ); + const opponent = matchState.players.find((player) => player.account._id !== userID); - if (JSON.stringify(topPlayerState) !== JSON.stringify(opponent)) - setTopPlayerState(opponent); + if (JSON.stringify(topPlayerState) !== JSON.stringify(opponent)) setTopPlayerState(opponent); } if (!me) { @@ -924,14 +950,7 @@ export default function ContextualizedMatchPage() { // TODO: Improve const transferCard = React.useCallback( - async function ( - cardID, - destinationZone, - originZone, - reveal, - shuffle, - index - ) { + async function (cardID, destinationZone, originZone, reveal, shuffle, index) { await sendRequest({ headers: { MatchID: matchState._id }, operation: 'transferCard', @@ -1033,7 +1052,12 @@ export default function ContextualizedMatchPage() { ); useSubscribe({ + // cleanup: () => { + // abortControllerRef.current.abort(); + // abortControllerRef.current = new AbortController(); + // }, connectionInfo: { matchID }, + dependencies: [matchID], queryString: matchQuery, setup: fetchMatchByID, subscriptionType: 'subscribeMatch', diff --git a/src/forms/CreateEventForm.jsx b/src/forms/CreateEventForm.jsx index 524879f..4a9b4cd 100644 --- a/src/forms/CreateEventForm.jsx +++ b/src/forms/CreateEventForm.jsx @@ -100,10 +100,7 @@ export default function CreateEventForm({ buds, cubes, open, toggleOpen }) { onClick={(event) => setCubeAnchorEl(event.currentTarget)} style={{ padding: 0 }} > - +
    )} - - {cube.name} - + {cube.name} ))} - + Event Type - } - label="Draft" - /> + } label="Draft" /> - } - label="Sealed" - /> + } label="Sealed" /> @@ -174,11 +157,7 @@ export default function CreateEventForm({ buds, cubes, open, toggleOpen }) {
    - + Buds @@ -204,18 +183,13 @@ export default function CreateEventForm({ buds, cubes, open, toggleOpen }) { prevState.filter((plr) => plr !== bud._id) ); } else { - setOtherPlayers((prevState) => [ - ...prevState, - bud._id - ]); + setOtherPlayers((prevState) => [...prevState, bud._id]); } }} value={bud._id} /> } - label={ - - } + label={} /> ))} @@ -225,11 +199,7 @@ export default function CreateEventForm({ buds, cubes, open, toggleOpen }) {
    - + Modules @@ -247,10 +217,7 @@ export default function CreateEventForm({ buds, cubes, open, toggleOpen }) { prevState.filter((mdl) => mdl !== module._id) ); } else { - setIncludedModules((prevState) => [ - ...prevState, - module._id - ]); + setIncludedModules((prevState) => [...prevState, module._id]); } }} value={module._id} @@ -315,9 +282,7 @@ export default function CreateEventForm({ buds, cubes, open, toggleOpen }) { disabled={posting} startIcon={(() => { if (posting) { - return ( - - ); + return ; } if (success) { return ; @@ -328,11 +293,7 @@ export default function CreateEventForm({ buds, cubes, open, toggleOpen }) { > Create - } - > + }> Cancel diff --git a/src/forms/CreateMatchForm.jsx b/src/forms/CreateMatchForm.jsx index 80bed9a..9efad36 100644 --- a/src/forms/CreateMatchForm.jsx +++ b/src/forms/CreateMatchForm.jsx @@ -75,9 +75,7 @@ export default function CreateMatchForm({ open, toggleOpen }) { ]) } > - + - } - label="Decks" - value={false} - /> - } - label="Event" - value={true} - /> + } label="Decks" value={false} /> + } label="Event" value={true} /> @@ -119,10 +109,7 @@ export default function CreateMatchForm({ open, toggleOpen }) { button onClick={(event) => setAnchorEl(event.currentTarget)} > - +
    - - {dck.name} - - - {dck.format} - + {dck.name} + {dck.format} } value={dck._id} @@ -196,14 +179,8 @@ export default function CreateMatchForm({ open, toggleOpen }) { key={plr.account._id} label={ - - - {plr.account.name} - + + {plr.account.name} } value={plr.account._id} @@ -217,14 +194,8 @@ export default function CreateMatchForm({ open, toggleOpen }) { key={bud._id} label={ - - - {bud.name} - + + {bud.name} } value={bud._id} @@ -241,9 +212,7 @@ export default function CreateMatchForm({ open, toggleOpen }) { component="fieldset" required={true} > - - Your Opponent's Deck - + Your Opponent's Deck setOpponentDeckID(event.target.value)} value={opponentDeckID} @@ -257,12 +226,8 @@ export default function CreateMatchForm({ open, toggleOpen }) { key={dck._id} label={ - - {dck.name} - - - {dck.format} - + {dck.name} + {dck.format} } value={dck._id} @@ -274,11 +239,7 @@ export default function CreateMatchForm({ open, toggleOpen }) { Whoop that Ass! - } - > + }> Cancel diff --git a/src/forms/DeleteCubeForm.jsx b/src/forms/DeleteCubeForm.jsx index 4b93b90..ecbb7c7 100644 --- a/src/forms/DeleteCubeForm.jsx +++ b/src/forms/DeleteCubeForm.jsx @@ -12,48 +12,36 @@ import MUITypography from '@mui/material/Typography'; import { useNavigate, useParams } from 'react-router-dom'; import deleteCube from '../graphql/mutations/cube/delete-cube'; -import { AccountContext } from '../contexts/account-context'; import { AuthenticationContext } from '../contexts/Authentication'; import { ErrorContext } from '../contexts/Error'; export default function DeleteCubeForm({ cubeToDelete, setCubeToDelete }) { - const { setAccountState } = useContext(AccountContext); const { userID } = useContext(AuthenticationContext); const { setErrorMessages } = useContext(ErrorContext); const navigate = useNavigate(); - const { accountID, cubeID } = useParams(); + const { cubeID } = useParams(); const [deleting, setDeleting] = useState(false); const [success, setSuccess] = useState(false); return ( - setCubeToDelete({ _id: null, name: null })} - > + setCubeToDelete({ _id: null, name: null })}> { event.preventDefault(); try { setDeleting(true); - const data = await deleteCube({ + await deleteCube({ headers: { CubeID: cubeToDelete._id }, queryString: '{\n_id\n}' }); setSuccess(true); - if (accountID) { - setAccountState((prevState) => ({ - ...prevState, - cubes: prevState.cubes.filter( - (cube) => cube._id !== data.data.deleteCube._id - ) - })); - } setTimeout(() => { setCubeToDelete({ _id: null, name: null }); if (cubeID) { navigate(`/account/${userID}`); } + setSuccess(false); }, 1000); } catch (error) { setErrorMessages((prevState) => [...prevState, error.message]); @@ -65,9 +53,7 @@ export default function DeleteCubeForm({ cubeToDelete, setCubeToDelete }) { {`Are you sure you want to delete "${cubeToDelete.name}"?`} - { - 'This action cannot be undone. You may want to export your list first.' - } + {'This action cannot be undone. You may want to export your list first.'} @@ -76,9 +62,7 @@ export default function DeleteCubeForm({ cubeToDelete, setCubeToDelete }) { disabled={deleting} startIcon={(() => { if (deleting) { - return ( - - ); + return ; } if (success) { return ; diff --git a/src/forms/DeleteDeckForm.jsx b/src/forms/DeleteDeckForm.jsx index b8c2d05..06ecb9f 100644 --- a/src/forms/DeleteDeckForm.jsx +++ b/src/forms/DeleteDeckForm.jsx @@ -12,48 +12,36 @@ import MUITypography from '@mui/material/Typography'; import { useNavigate, useParams } from 'react-router-dom'; import deleteDeck from '../graphql/mutations/deck/delete-deck'; -import { AccountContext } from '../contexts/account-context'; import { AuthenticationContext } from '../contexts/Authentication'; import { ErrorContext } from '../contexts/Error'; export default function DeleteDeckForm({ deckToDelete, setDeckToDelete }) { - const { setAccountState } = useContext(AccountContext); const { userID } = useContext(AuthenticationContext); const { setErrorMessages } = useContext(ErrorContext); const navigate = useNavigate(); - const { accountID, deckID } = useParams(); + const { deckID } = useParams(); const [deleting, setDeleting] = useState(false); const [success, setSuccess] = useState(false); return ( - setDeckToDelete({ _id: null, name: null })} - > + setDeckToDelete({ _id: null, name: null })}> { event.preventDefault(); try { setDeleting(true); - const data = await deleteDeck({ + await deleteDeck({ headers: { DeckID: deckToDelete._id }, queryString: '{\n_id\n}' }); setSuccess(true); - if (accountID) { - setAccountState((prevState) => ({ - ...prevState, - decks: prevState.decks.filter( - (deck) => deck._id !== data.data.deleteDeck._id - ) - })); - } setTimeout(() => { setDeckToDelete({ _id: null, name: null }); if (deckID) { navigate(`/account/${userID}`); } + setSuccess(false); }, 1000); } catch (error) { setErrorMessages((prevState) => [...prevState, error.message]); @@ -65,9 +53,7 @@ export default function DeleteDeckForm({ deckToDelete, setDeckToDelete }) { {`Are you sure you want to delete "${deckToDelete.name}"?`} - { - 'This action cannot be undone. You may want to export your list first.' - } + {'This action cannot be undone. You may want to export your list first.'} @@ -76,9 +62,7 @@ export default function DeleteDeckForm({ deckToDelete, setDeckToDelete }) { disabled={deleting} startIcon={(() => { if (deleting) { - return ( - - ); + return ; } if (success) { return ; diff --git a/src/functions/cache-scryfall-data.js b/src/functions/cache-scryfall-data.js index e18e3b9..dc54a4c 100644 --- a/src/functions/cache-scryfall-data.js +++ b/src/functions/cache-scryfall-data.js @@ -1,11 +1,12 @@ export default async function cacheScryfallData(scryfallCardData) { - let art_crop, back_image, image, mana_cost, meldResult, type_line; + let art_crop, back_image, image, mana_cost, meldResult, oracle_text, type_line; switch (scryfallCardData.layout) { case 'adventure': // this mechanic debuted in Throne of Eldrain. all adventure cards are either (instants or sorceries) and creatures. it seems to have been popular, so it may appear again art_crop = scryfallCardData.image_uris.art_crop; image = scryfallCardData.image_uris.large; mana_cost = `${scryfallCardData.card_faces[0].mana_cost}${scryfallCardData.card_faces[1].mana_cost}`; + oracle_text = `${scryfallCardData.card_faces[0].oracle_text} / ${scryfallCardData.card_faces[1].oracle_text}`; type_line = `${scryfallCardData.card_faces[0].type_line} / ${scryfallCardData.card_faces[1].type_line}`; break; case 'flip': @@ -13,6 +14,7 @@ export default async function cacheScryfallData(scryfallCardData) { art_crop = scryfallCardData.image_uris.art_crop; image = scryfallCardData.image_uris.large; mana_cost = scryfallCardData.card_faces[0].mana_cost; + oracle_text = `${scryfallCardData.card_faces[0].oracle_text} / ${scryfallCardData.card_faces[1].oracle_text}`; type_line = `${scryfallCardData.card_faces[0].type_line} / ${scryfallCardData.card_faces[1].type_line}`; break; case 'leveler': @@ -20,6 +22,7 @@ export default async function cacheScryfallData(scryfallCardData) { art_crop = scryfallCardData.image_uris.art_crop; image = scryfallCardData.image_uris.large; mana_cost = scryfallCardData.mana_cost; + oracle_text = scryfallCardData.oracle_text; type_line = scryfallCardData.type_line; break; case 'meld': @@ -29,18 +32,18 @@ export default async function cacheScryfallData(scryfallCardData) { type_line = scryfallCardData.type_line; meldResult = await fetch( - scryfallCardData.all_parts.find( - (part) => part.component === 'meld_result' - ).uri + scryfallCardData.all_parts.find((part) => part.component === 'meld_result').uri ).json(); back_image = meldResult.image_uris.large; image = scryfallCardData.image_uris.large; + oracle_text = `${scryfallCardData.oracle_text} / ${meldResult.oracle_text}`; break; case 'modal_dfc': art_crop = scryfallCardData.card_faces[0].image_uris.art_crop; back_image = scryfallCardData.card_faces[1].image_uris.large; image = scryfallCardData.card_faces[0].image_uris.large; mana_cost = `${scryfallCardData.card_faces[0].mana_cost}${scryfallCardData.card_faces[1].mana_cost}`; + oracle_text = `${scryfallCardData.card_faces[0].oracle_text} / ${scryfallCardData.card_faces[1].oracle_text}`; type_line = `${scryfallCardData.card_faces[0].type_line} / ${scryfallCardData.card_faces[1].type_line}`; break; case 'saga': @@ -48,6 +51,7 @@ export default async function cacheScryfallData(scryfallCardData) { art_crop = scryfallCardData.image_uris.art_crop; image = scryfallCardData.image_uris.large; mana_cost = scryfallCardData.mana_cost; + oracle_text = scryfallCardData.oracle_text; type_line = scryfallCardData.type_line; break; case 'split': @@ -55,6 +59,7 @@ export default async function cacheScryfallData(scryfallCardData) { art_crop = scryfallCardData.image_uris.art_crop; image = scryfallCardData.image_uris.large; mana_cost = `${scryfallCardData.card_faces[0].mana_cost}${scryfallCardData.card_faces[1].mana_cost}`; + oracle_text = `${scryfallCardData.card_faces[0].oracle_text} / ${scryfallCardData.card_faces[1].oracle_text}`; type_line = `${scryfallCardData.card_faces[0].type_line} / ${scryfallCardData.card_faces[1].type_line}`; break; case 'transform': @@ -62,12 +67,14 @@ export default async function cacheScryfallData(scryfallCardData) { back_image = scryfallCardData.card_faces[1].image_uris.large; image = scryfallCardData.card_faces[0].image_uris.large; mana_cost = scryfallCardData.card_faces[0].mana_cost; + oracle_text = `${scryfallCardData.card_faces[0].oracle_text} / ${scryfallCardData.card_faces[1].oracle_text}`; type_line = `${scryfallCardData.card_faces[0].type_line} / ${scryfallCardData.card_faces[1].type_line}`; break; default: art_crop = scryfallCardData.image_uris.art_crop; image = scryfallCardData.image_uris.large; mana_cost = scryfallCardData.mana_cost; + oracle_text = scryfallCardData.oracle_text; type_line = scryfallCardData.type_line; } @@ -79,10 +86,12 @@ export default async function cacheScryfallData(scryfallCardData) { color_identity: scryfallCardData.color_identity, image, keywords: scryfallCardData.keywords, + legalities: scryfallCardData.legalities, mana_cost, mtgo_id: scryfallCardData.mtgo_id, name: scryfallCardData.name, oracle_id: scryfallCardData.oracle_id, + oracle_text, scryfall_id: scryfallCardData.id, set: scryfallCardData.set, set_name: scryfallCardData.set_name, diff --git a/src/functions/custom-sort.js b/src/functions/custom-sort.js index b4c2ccd..0a0ced5 100644 --- a/src/functions/custom-sort.js +++ b/src/functions/custom-sort.js @@ -1,8 +1,14 @@ +function getValue(obj, path) { + if (!path) return obj; + const properties = path.split('.'); + return getValue(obj[properties.shift()], properties.join('.')); +} + export default function customSort(objectsToSort, propertiesToSortBy) { objectsToSort.sort(function (a, b) { for (let property of propertiesToSortBy) { - if (a[property] > b[property]) return 1; - if (b[property] > a[property]) return -1; + if (getValue(a, property) > getValue(b, property)) return 1; + if (getValue(b, property) > getValue(a, property)) return -1; } return 0; diff --git a/src/functions/generate-csv-list.js b/src/functions/generate-csv-list.js index b2a0964..491f8ef 100644 --- a/src/functions/generate-csv-list.js +++ b/src/functions/generate-csv-list.js @@ -1,13 +1,26 @@ -export default function generateCSVList(mainboard, sideboard) { +export default function generateCSVList(cards) { + let mainboard = ''; + let sideboard = ''; + + for (const card of cards) { + const { + mainboard_count, + scryfall_card: { mtgo_id, name, rarity, _set }, + sideboard_count + } = card; + if (mainboard_count > 0) { + mainboard += `"${name.split(' // ')[0]}",${mainboard_count},${mtgo_id ? mtgo_id : ' '},${ + rarity.charAt(0).toUpperCase() + rarity.slice(1) + },${_set.toUpperCase()}, , ,No,0\n`; + } + if (sideboard_count > 0) { + sideboard += `"${name.split(' // ')[0]}",${sideboard_count},${mtgo_id ? mtgo_id : ' '},${ + rarity.charAt(0).toUpperCase() + rarity.slice(1) + },${_set.toUpperCase()}, , ,Yes,0\n`; + } + } + return 'Card Name,Quantity,ID #,Rarity,Set,Collector #,Premium,Sideboarded,Annotation\n' - .concat( - mainboard.reduce(function (a, c) { - return `${a}"${c.name.split(' // ')[0]}",1,${c.mtgo_id ? c.mtgo_id : ' '}, , , , ,No,0\n`; - }, '') - ) - .concat( - sideboard.reduce(function (a, c) { - return `${a}"${c.name.split(' // ')[0]}",1,${c.mtgo_id ? c.mtgo_id : ' '}, , , , ,Yes,0\n`; - }, '') - ); + .concat(mainboard) + .concat(sideboard); } diff --git a/src/functions/validate-deck.js b/src/functions/validate-deck.js new file mode 100644 index 0000000..07983c9 --- /dev/null +++ b/src/functions/validate-deck.js @@ -0,0 +1,76 @@ +import classyBannedList from '../constants/classy-banned-list'; + +export default function validateDeck({ format, mainboard, sideboard }, setWarnings) { + const validationIssues = []; + switch (format) { + case 'Classy': + if (mainboard.length < 69) { + validationIssues.push( + `Mainboard does not contain enough cards! 69 cards are required, but there are currently only ${mainboard.length}.` + ); + } + if (mainboard.length > 69) { + validationIssues.push( + `Mainboard contains too many cards! 69 cards are required, but there are currently ${mainboard.length}.` + ); + } + if (sideboard.length > 21) { + validationIssues.push( + `Sideboard contains too many cards! 21 is the limit, but there are currently ${sideboard.length}.` + ); + } + + const cardCountObject = {}; + + for (const card of mainboard.concat(sideboard)) { + if (card.scryfall_card.name in cardCountObject) { + cardCountObject[card.scryfall_card.name].count++; + } else { + cardCountObject[card.scryfall_card.name] = { + ...card.scryfall_card, + count: 1 + }; + } + } + + for (const [name, cardObject] of Object.entries(cardCountObject)) { + if (cardObject.legalities.not_legal.includes('modern')) { + validationIssues.push( + `Deck contains ${cardObject.count} cop${ + cardObject.count === 1 ? 'y' : 'ies' + } of "${name}", which has never been printed into a set that is legal in Classy!` + ); + } else if (Object.keys(classyBannedList).includes(name)) { + validationIssues.push( + `Deck contains ${cardObject.count} cop${ + cardObject.count === 1 ? 'y' : 'ies' + } of the banned card "${name}"!` + ); + } else if (cardObject.count > 1) { + if (cardObject.type_line.includes('Legendary')) { + validationIssues.push( + `Deck contains ${cardObject.count} copies of the Legendary card "${name}"! A maximum of 1 copy is allowed.` + ); + } else if ( + cardObject.type_line.includes('Land') && + !cardObject.type_line.includes('Basic') + ) { + validationIssues.push( + `Deck contains ${cardObject.count} copies of the nonbasic land "${name}"! A maximum of 1 copy is allowed.` + ); + } else if ( + cardObject.count > 3 && + !cardObject.type_line.includes('Basic') && + !cardObject.oracle_text?.includes(`A deck can have any number of cards named ${name}`) + ) { + validationIssues.push( + `Deck contains ${cardObject.count} copies of "${name}"! A maximum of 3 copies are allowed.` + ); + } + } + } + break; + default: + } + setWarnings(validationIssues); +} diff --git a/src/graphql/mutations/account/edit-account.js b/src/graphql/mutations/account/edit-account.js new file mode 100644 index 0000000..ce8193a --- /dev/null +++ b/src/graphql/mutations/account/edit-account.js @@ -0,0 +1,80 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncEditAccount({ + queryString, + signal, + variables: { avatar, email, measurement_system, name, radius } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($avatar: String, $email: String, $measurement_system: MeasurementSystemEnum, $name: String, $radius: Int) { + editAccount (avatar: $avatar, email: $email, measurement_system: $measurement_system, name: $name, radius: $radius) ${queryString} + } + `, + variables: { + avatar, + email, + measurement_system, + name, + radius + } + }, + signal + }); +} + +export function syncEditAccount({ + variables: { avatar, email, measurement_system, name, radius } +}) { + syncFancyFetch({ + body: { + query: ` + mutation($avatar: String, $email: String, $measurement_system: MeasurementSystemEnum, $name: String, $radius: Int) { + editAccount (avatar: $avatar, email: $email, measurement_system: $measurement_system, name: $name, radius: $radius) { + _id + } + } + `, + variables: { + avatar, + email, + measurement_system, + name, + radius + } + } + }); +} + +export default function editAccount({ + queryString, + signal, + variables: { avatar, email, measurement_system, name, radius } +}) { + if (queryString) { + return (async function () { + return await asyncEditAccount({ + queryString, + signal, + variables: { + avatar, + email, + measurement_system, + name, + radius + } + }); + })(); + } else { + syncEditAccount({ + variables: { + avatar, + email, + measurement_system, + name, + radius + } + }); + } +} diff --git a/src/graphql/mutations/account/initiate-bud-request.js b/src/graphql/mutations/account/initiate-bud-request.js new file mode 100644 index 0000000..5851eb5 --- /dev/null +++ b/src/graphql/mutations/account/initiate-bud-request.js @@ -0,0 +1,50 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncInitiateBudRequest({ + queryString, + signal, + variables: { other_user_id } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($other_user_id: String!) { + initiateBudRequest (other_user_id: $other_user_id) ${queryString} + } + `, + variables: { other_user_id } + }, + signal + }); +} + +export function syncInitiateBudRequest({ variables: { other_user_id } }) { + syncFancyFetch({ + body: { + query: ` + mutation($other_user_id: String!) { + initiateBudRequest (other_user_id: $other_user_id) { + _id + } + } + `, + variables: { other_user_id } + } + }); +} + +export default function initiateBudRequest({ queryString, signal, variables: { other_user_id } }) { + if (queryString) { + return (async function () { + return await asyncInitiateBudRequest({ + queryString, + signal, + variables: { other_user_id } + }); + })(); + } else { + syncInitiateBudRequest({ + variables: { other_user_id } + }); + } +} diff --git a/src/graphql/mutations/account/login.js b/src/graphql/mutations/account/login.js new file mode 100644 index 0000000..8a5d12b --- /dev/null +++ b/src/graphql/mutations/account/login.js @@ -0,0 +1,46 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncLogin({ queryString, signal, variables: { email, password } }) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($email: String!, $password: String!) { + login (email: $email, password: $password) ${queryString} + } + `, + variables: { email, password } + }, + signal + }); +} + +export function syncLogin({ variables: { email, password } }) { + syncFancyFetch({ + body: { + query: ` + mutation($email: String!, $password: String!) { + login (email: $email, password: $password) { + _id + } + } + `, + variables: { email, password } + } + }); +} + +export default function login({ queryString, signal, variables: { email, password } }) { + if (queryString) { + return (async function () { + return await asyncLogin({ + queryString, + signal, + variables: { email, password } + }); + })(); + } else { + syncLogin({ + variables: { email, password } + }); + } +} diff --git a/src/graphql/mutations/account/logout-single-device.js b/src/graphql/mutations/account/logout-single-device.js new file mode 100644 index 0000000..8001e2a --- /dev/null +++ b/src/graphql/mutations/account/logout-single-device.js @@ -0,0 +1,63 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncLogoutSingleDevice({ signal, variables: { endpoint } }) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($endpoint: String) { + logoutSingleDevice (endpoint: $endpoint) + } + `, + variables: { endpoint } + }, + signal + }); +} + +export function syncLogoutSingleDevice({ variables: { endpoint } }) { + syncFancyFetch({ + body: { + query: ` + mutation($endpoint: String) { + logoutSingleDevice (endpoint: $endpoint) + } + `, + variables: { endpoint } + } + }); +} + +export default async function logoutSingleDevice({ signal }) { + // unsubscribe from push notifications if subscribed + let subscription; + + if ('Notification' in window && 'serviceWorker' in navigator) { + const swreg = await navigator.serviceWorker.ready; + subscription = await swreg.pushManager.getSubscription(); + if (subscription) { + try { + await subscription.unsubscribe(); + } catch (error) { + throw new Error(error.message); + } + } + } + + // if the logged in user had a push subscription, remove it and the token from the server + if (signal) { + return (async function () { + return await asyncLogoutSingleDevice({ + signal, + variables: { + endpoint: subscription ? subscription.endpoint : undefined + } + }); + })(); + } else { + syncLogoutSingleDevice({ + variables: { + endpoint: subscription ? subscription.endpoint : undefined + } + }); + } +} diff --git a/src/graphql/mutations/account/register.js b/src/graphql/mutations/account/register.js new file mode 100644 index 0000000..0f3e5c5 --- /dev/null +++ b/src/graphql/mutations/account/register.js @@ -0,0 +1,46 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncRegister({ queryString, signal, variables: { email, name, password } }) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($email: String!, $name: String!, $password: String!) { + register (email: $email, name: $name, password: $password) ${queryString} + } + `, + variables: { email, name, password } + }, + signal + }); +} + +export function syncRegister({ variables: { email, name, password } }) { + syncFancyFetch({ + body: { + query: ` + mutation($email: String!, $name: String!, $password: String!) { + register (email: $email, name: $name, password: $password) { + _id + } + } + `, + variables: { email, name, password } + } + }); +} + +export default function register({ queryString, signal, variables: { email, name, password } }) { + if (queryString) { + return (async function () { + return await asyncRegister({ + queryString, + signal, + variables: { email, name, password } + }); + })(); + } else { + syncRegister({ + variables: { email, name, password } + }); + } +} diff --git a/src/graphql/mutations/account/request-password-reset.js b/src/graphql/mutations/account/request-password-reset.js new file mode 100644 index 0000000..b35c5cd --- /dev/null +++ b/src/graphql/mutations/account/request-password-reset.js @@ -0,0 +1,43 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncRequestPasswordReset({ signal, variables: { email } }) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($email: String!) { + requestPasswordReset (email: $email) + } + `, + variables: { email } + }, + signal + }); +} + +export function syncRequestPasswordReset({ variables: { email } }) { + syncFancyFetch({ + body: { + query: ` + mutation($email: String!) { + requestPasswordReset (email: $email) + } + `, + variables: { email } + } + }); +} + +export default function requestPasswordReset({ signal, variables: { email } }) { + if (signal) { + return (async function () { + return await asyncRequestPasswordReset({ + signal, + variables: { email } + }); + })(); + } else { + syncRequestPasswordReset({ + variables: { email } + }); + } +} diff --git a/src/graphql/mutations/account/respond-to-bud-request.js b/src/graphql/mutations/account/respond-to-bud-request.js new file mode 100644 index 0000000..1c54643 --- /dev/null +++ b/src/graphql/mutations/account/respond-to-bud-request.js @@ -0,0 +1,54 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncRespondToBudRequest({ + queryString, + signal, + variables: { other_user_id, response } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($other_user_id: String!, $response: BudRequestResponseEnum!) { + respondToBudRequest (other_user_id: $other_user_id) ${queryString} + } + `, + variables: { other_user_id, response } + }, + signal + }); +} + +export function syncRespondToBudRequest({ variables: { other_user_id, response } }) { + syncFancyFetch({ + body: { + query: ` + mutation($other_user_id: String!, $response: BudRequestResponseEnum!) { + respondToBudRequest (other_user_id: $other_user_id) { + _id + } + } + `, + variables: { other_user_id, response } + } + }); +} + +export default function respondToBudRequest({ + queryString, + signal, + variables: { other_user_id, response } +}) { + if (queryString) { + return (async function () { + return await asyncRespondToBudRequest({ + queryString, + signal, + variables: { other_user_id, response } + }); + })(); + } else { + syncRespondToBudRequest({ + variables: { other_user_id, response } + }); + } +} diff --git a/src/graphql/mutations/account/revoke-budship.js b/src/graphql/mutations/account/revoke-budship.js new file mode 100644 index 0000000..2528822 --- /dev/null +++ b/src/graphql/mutations/account/revoke-budship.js @@ -0,0 +1,46 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncRevokeBudship({ queryString, signal, variables: { other_user_id } }) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($other_user_id: String!) { + revokeBudship (other_user_id: $other_user_id) ${queryString} + } + `, + variables: { other_user_id } + }, + signal + }); +} + +export function syncRevokeBudship({ variables: { other_user_id } }) { + syncFancyFetch({ + body: { + query: ` + mutation($other_user_id: String!) { + revokeBudship (other_user_id: $other_user_id) { + _id + } + } + `, + variables: { other_user_id } + } + }); +} + +export default function revokeBudship({ queryString, signal, variables: { other_user_id } }) { + if (queryString) { + return (async function () { + return await asyncRevokeBudship({ + queryString, + signal, + variables: { other_user_id } + }); + })(); + } else { + syncRevokeBudship({ + variables: { other_user_id } + }); + } +} diff --git a/src/graphql/mutations/account/submit-password-reset.js b/src/graphql/mutations/account/submit-password-reset.js new file mode 100644 index 0000000..1aa9ad0 --- /dev/null +++ b/src/graphql/mutations/account/submit-password-reset.js @@ -0,0 +1,54 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncSubmitPasswordReset({ + queryString, + signal, + variables: { email, password, reset_token } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($email: String!, $password: String!, $reset_token: String!) { + submitPasswordReset (email: $email, password: $password, reset_token: $reset_token) ${queryString} + } + `, + variables: { email, password, reset_token } + }, + signal + }); +} + +export function syncSubmitPasswordReset({ variables: { email, password, reset_token } }) { + syncFancyFetch({ + body: { + query: ` + mutation($email: String!, $password: String!, $reset_token: String!) { + submitPasswordReset (email: $email, password: $password, reset_token: $reset_token) { + _id + } + } + `, + variables: { email, password, reset_token } + } + }); +} + +export default function submitPasswordReset({ + queryString, + signal, + variables: { email, password, reset_token } +}) { + if (queryString) { + return (async function () { + return await asyncSubmitPasswordReset({ + queryString, + signal, + variables: { email, password, reset_token } + }); + })(); + } else { + syncSubmitPasswordReset({ + variables: { email, password, reset_token } + }); + } +} diff --git a/src/graphql/mutations/blog/create-comment.js b/src/graphql/mutations/blog/create-comment.js new file mode 100644 index 0000000..0f7ef7b --- /dev/null +++ b/src/graphql/mutations/blog/create-comment.js @@ -0,0 +1,60 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncCreateComment({ + headers: { BlogPostID }, + queryString, + signal, + variables: { body } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($body: String!) { + createComment (body: $body) ${queryString} + } + `, + variables: { body } + }, + headers: { BlogPostID }, + signal + }); +} + +export function syncCreateComment({ headers: { BlogPostID }, variables: { body } }) { + syncFancyFetch({ + body: { + query: ` + mutation($body: String!) { + createComment (body: $body) { + _id + } + } + `, + variables: { body } + }, + headers: { BlogPostID } + }); +} + +export default function createComment({ + headers: { BlogPostID }, + queryString, + signal, + variables: { body } +}) { + if (queryString) { + return (async function () { + return await asyncCreateComment({ + headers: { BlogPostID }, + queryString, + signal, + variables: { body } + }); + })(); + } else { + syncCreateComment({ + headers: { BlogPostID }, + variables: { body } + }); + } +} diff --git a/src/graphql/mutations/conversation/create-conversation-message.js b/src/graphql/mutations/conversation/create-conversation-message.js new file mode 100644 index 0000000..a54a379 --- /dev/null +++ b/src/graphql/mutations/conversation/create-conversation-message.js @@ -0,0 +1,66 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncCreateConversationMessage({ + headers, + queryString, + signal, + variables: { body, participants } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($body: String!, $participants: [String]) { + createConversationMessage (body: $body, participants: $participants) ${queryString} + } + `, + variables: { body, participants } + }, + headers, + signal + }); +} + +export function syncCreateConversationMessage({ headers, variables: { body, participants } }) { + syncFancyFetch({ + body: { + query: ` + mutation($body: String!, $participants: [String]) { + createConversationMessage (body: $body, participants: $participants) { + _id + } + } + `, + variables: { body, participants } + }, + headers + }); +} + +export default function createConversationMessage({ + headers, + queryString, + signal, + variables: { body, participants } +}) { + let ConversationID; + + if (headers) { + ConversationID = headers.ConversationID; + } + + if (queryString) { + return (async function () { + return await asyncCreateConversationMessage({ + headers: ConversationID ? { ConversationID } : {}, + queryString, + signal, + variables: { body, participants } + }); + })(); + } else { + syncCreateConversationMessage({ + headers: ConversationID ? { ConversationID } : {}, + variables: { body, participants } + }); + } +} diff --git a/src/graphql/mutations/cube/add-card-to-cube.js b/src/graphql/mutations/cube/add-card-to-cube.js index 28dd92d..e574913 100644 --- a/src/graphql/mutations/cube/add-card-to-cube.js +++ b/src/graphql/mutations/cube/add-card-to-cube.js @@ -13,7 +13,7 @@ export async function asyncAddCardToCube({ body: { query: ` mutation($componentID: String!, $scryfall_id: String!) { - addCardToCube (componentID: $componentID, name: $name, scryfall_id: $scryfall_id) ${queryString} + addCardToCube (componentID: $componentID, scryfall_id: $scryfall_id) ${queryString} } `, variables: { componentID, scryfall_id } diff --git a/src/graphql/mutations/cube/clone-cube.js b/src/graphql/mutations/cube/clone-cube.js new file mode 100644 index 0000000..2fc70e6 --- /dev/null +++ b/src/graphql/mutations/cube/clone-cube.js @@ -0,0 +1,57 @@ +import { + asyncFancyFetch, + syncFancyFetch +} from '../../../functions/fancy-fetches'; + +export async function asyncCloneCube({ + headers: { CubeID }, + queryString, + signal +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation { + cloneCube ${queryString} + } + ` + }, + headers: { CubeID }, + signal + }); +} + +export function syncCloneCube({ headers: { CubeID } }) { + syncFancyFetch({ + body: { + query: ` + mutation { + cloneCube { + _id + } + } + ` + }, + headers: { CubeID } + }); +} + +export default function cloneCube({ + headers: { CubeID }, + queryString, + signal +}) { + if (queryString) { + return (async function () { + return await asyncCloneCube({ + headers: { CubeID }, + queryString, + signal + }); + })(); + } else { + syncCloneCube({ + headers: { CubeID } + }); + } +} diff --git a/src/graphql/mutations/cube/edit-card.js b/src/graphql/mutations/cube/edit-card.js new file mode 100644 index 0000000..d0a6e46 --- /dev/null +++ b/src/graphql/mutations/cube/edit-card.js @@ -0,0 +1,122 @@ +import { + asyncFancyFetch, + syncFancyFetch +} from '../../../functions/fancy-fetches'; + +export async function asyncEditCard({ + headers: { CubeID }, + queryString, + signal, + variables: { + cardID, + componentID, + cmc, + color_identity, + notes, + scryfall_id, + type_line + } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($cardID: ID!, $componentID: ID!, $cmc: Int, $color_identity: [String!], $notes: String, $scryfall_id: String, $type_line: String) { + editCard (cardID: $cardID, componentID: $componentID, cmc: $cmc, color_identity: $color_identity, notes: $notes, scryfall_id: $scryfall_id, type_line: $type_line) ${queryString} + } + `, + variables: { + cardID, + componentID, + cmc, + color_identity, + notes, + scryfall_id, + type_line + } + }, + headers: { CubeID }, + signal + }); +} + +export function syncEditCard({ + headers: { CubeID }, + variables: { + cardID, + componentID, + cmc, + color_identity, + notes, + scryfall_id, + type_line + } +}) { + syncFancyFetch({ + body: { + query: ` + mutation($cardID: ID!, $componentID: ID!, $cmc: Int, $color_identity: [String!], $notes: String, $scryfall_id: String, $type_line: String) { + editCard (cardID: $cardID, componentID: $componentID, cmc: $cmc, color_identity: $color_identity, notes: $notes, scryfall_id: $scryfall_id, type_line: $type_line) { + _id + } + } + `, + variables: { + cardID, + componentID, + cmc, + color_identity, + notes, + scryfall_id, + type_line + } + }, + headers: { CubeID } + }); +} + +export default function editCard({ + headers: { CubeID }, + queryString, + signal, + variables: { + cardID, + componentID, + cmc, + color_identity, + notes, + scryfall_id, + type_line + } +}) { + if (queryString) { + return (async function () { + return await asyncEditCard({ + headers: { CubeID }, + queryString, + signal, + variables: { + cardID, + componentID, + cmc, + color_identity, + notes, + scryfall_id, + type_line + } + }); + })(); + } else { + syncEditCard({ + headers: { CubeID }, + variables: { + cardID, + componentID, + cmc, + color_identity, + notes, + scryfall_id, + type_line + } + }); + } +} diff --git a/src/graphql/mutations/cube/edit-cube.js b/src/graphql/mutations/cube/edit-cube.js new file mode 100644 index 0000000..f0ddd02 --- /dev/null +++ b/src/graphql/mutations/cube/edit-cube.js @@ -0,0 +1,66 @@ +import { + asyncFancyFetch, + syncFancyFetch +} from '../../../functions/fancy-fetches'; + +export async function asyncEditCube({ + headers: { CubeID }, + queryString, + signal, + variables: { description, image, name, published } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($description: String, $image: String, $name: String, $published: Boolean) { + editCube (description: $description, image: $image, name: $name, published: $published) ${queryString} + } + `, + variables: { description, image, name, published } + }, + headers: { CubeID }, + signal + }); +} + +export function syncEditCube({ + headers: { CubeID }, + variables: { description, image, name, published } +}) { + syncFancyFetch({ + body: { + query: ` + mutation($description: String, $image: String, $name: String, $published: Boolean) { + editCube (description: $description, image: $image, name: $name, published: $published) { + _id + } + } + `, + variables: { description, image, name, published } + }, + headers: { CubeID } + }); +} + +export default function editCube({ + headers: { CubeID }, + queryString, + signal, + variables: { description, image, name, published } +}) { + if (queryString) { + return (async function () { + return await asyncEditCube({ + headers: { CubeID }, + queryString, + signal, + variables: { description, image, name, published } + }); + })(); + } else { + syncEditCube({ + headers: { CubeID }, + variables: { description, image, name, published } + }); + } +} diff --git a/src/graphql/mutations/cube/edit-module.js b/src/graphql/mutations/cube/edit-module.js new file mode 100644 index 0000000..baeed7a --- /dev/null +++ b/src/graphql/mutations/cube/edit-module.js @@ -0,0 +1,66 @@ +import { + asyncFancyFetch, + syncFancyFetch +} from '../../../functions/fancy-fetches'; + +export async function asyncEditModule({ + headers: { CubeID }, + queryString, + signal, + variables: { moduleID, name } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($moduleID: ID!, $name: String) { + editModule (moduleID: $moduleID, name: $name) ${queryString} + } + `, + variables: { moduleID, name } + }, + headers: { CubeID }, + signal + }); +} + +export function syncEditModule({ + headers: { CubeID }, + variables: { moduleID, name } +}) { + syncFancyFetch({ + body: { + query: ` + mutation($moduleID: ID!, $name: String) { + editModule (moduleID: $moduleID, name: $name) { + _id + } + } + `, + variables: { moduleID, name } + }, + headers: { CubeID } + }); +} + +export default function editModule({ + headers: { CubeID }, + queryString, + signal, + variables: { moduleID, name } +}) { + if (queryString) { + return (async function () { + return await asyncEditModule({ + headers: { CubeID }, + queryString, + signal, + variables: { moduleID, name } + }); + })(); + } else { + syncEditModule({ + headers: { CubeID }, + variables: { moduleID, name } + }); + } +} diff --git a/src/graphql/mutations/deck/clone-deck.js b/src/graphql/mutations/deck/clone-deck.js new file mode 100644 index 0000000..b85d66f --- /dev/null +++ b/src/graphql/mutations/deck/clone-deck.js @@ -0,0 +1,46 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncCloneDeck({ headers: { DeckID }, queryString, signal }) { + return await asyncFancyFetch({ + body: { + query: ` + mutation { + cloneDeck ${queryString} + } + ` + }, + headers: { DeckID }, + signal + }); +} + +export function syncCloneDeck({ headers: { DeckID } }) { + syncFancyFetch({ + body: { + query: ` + mutation { + cloneDeck { + _id + } + } + ` + }, + headers: { DeckID } + }); +} + +export default function cloneDeck({ headers: { DeckID }, queryString, signal }) { + if (queryString) { + return (async function () { + return await asyncCloneDeck({ + headers: { DeckID }, + queryString, + signal + }); + })(); + } else { + syncCloneDeck({ + headers: { DeckID } + }); + } +} diff --git a/src/graphql/mutations/deck/edit-deck.js b/src/graphql/mutations/deck/edit-deck.js new file mode 100644 index 0000000..226ffd8 --- /dev/null +++ b/src/graphql/mutations/deck/edit-deck.js @@ -0,0 +1,63 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncEditDeck({ + headers: { DeckID }, + queryString, + signal, + variables: { description, format, image, name, published } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($description: String, $format: FormatEnum, $image: String, $name: String, $published: Boolean) { + editDeck (description: $description, format: $format, image: $image, name: $name, published: $published) ${queryString} + } + `, + variables: { description, format, image, name, published } + }, + headers: { DeckID }, + signal + }); +} + +export function syncEditDeck({ + headers: { DeckID }, + variables: { description, format, image, name, published } +}) { + syncFancyFetch({ + body: { + query: ` + mutation($description: String, $format: FormatEnum, $image: String, $name: String, $published: Boolean) { + editDeck (description: $description, format: $format, image: $image, name: $name, published: $published) { + _id + } + } + `, + variables: { description, format, image, name, published } + }, + headers: { DeckID } + }); +} + +export default function editDeck({ + headers: { DeckID }, + queryString, + signal, + variables: { description, format, image, name, published } +}) { + if (queryString) { + return (async function () { + return await asyncEditDeck({ + headers: { DeckID }, + queryString, + signal, + variables: { description, format, image, name, published } + }); + })(); + } else { + syncEditDeck({ + headers: { DeckID }, + variables: { description, format, image, name, published } + }); + } +} diff --git a/src/graphql/mutations/deck/set-number-of-deck-card-copies.js b/src/graphql/mutations/deck/set-number-of-deck-card-copies.js new file mode 100644 index 0000000..bd7f083 --- /dev/null +++ b/src/graphql/mutations/deck/set-number-of-deck-card-copies.js @@ -0,0 +1,63 @@ +import { asyncFancyFetch, syncFancyFetch } from '../../../functions/fancy-fetches'; + +export async function asyncSetNumberOfDeckCardCopies({ + headers: { DeckID }, + queryString, + signal, + variables: { mainboard_count, maybeboard_count, scryfall_id, sideboard_count } +}) { + return await asyncFancyFetch({ + body: { + query: ` + mutation($mainboard_count: Int!, $maybeboard_count: Int!, $scryfall_id: String!, $sideboard_count: Int!) { + setNumberOfDeckCardCopies (mainboard_count: $mainboard_count, maybeboard_count: $maybeboard_count, scryfall_id: $scryfall_id, sideboard_count: $sideboard_count) ${queryString} + } + `, + variables: { mainboard_count, maybeboard_count, scryfall_id, sideboard_count } + }, + headers: { DeckID }, + signal + }); +} + +export function syncSetNumberOfDeckCardCopies({ + headers: { DeckID }, + variables: { mainboard_count, maybeboard_count, scryfall_id, sideboard_count } +}) { + syncFancyFetch({ + body: { + query: ` + mutation($mainboard_count: Int!, $maybeboard_count: Int!, $scryfall_id: String!, $sideboard_count: Int!) { + setNumberOfDeckCardCopies (mainboard_count: $mainboard_count, maybeboard_count: $maybeboard_count, scryfall_id: $scryfall_id, sideboard_count: $sideboard_count) { + _id + } + } + `, + variables: { mainboard_count, maybeboard_count, scryfall_id, sideboard_count } + }, + headers: { DeckID } + }); +} + +export default function setNumberOfDeckCardCopies({ + headers: { DeckID }, + queryString, + signal, + variables: { mainboard_count, maybeboard_count, scryfall_id, sideboard_count } +}) { + if (queryString) { + return (async function () { + return await asyncSetNumberOfDeckCardCopies({ + headers: { DeckID }, + queryString, + signal, + variables: { mainboard_count, maybeboard_count, scryfall_id, sideboard_count } + }); + })(); + } else { + syncSetNumberOfDeckCardCopies({ + headers: { DeckID }, + variables: { mainboard_count, maybeboard_count, scryfall_id, sideboard_count } + }); + } +} diff --git a/src/graphql/queries/account/authenticate.js b/src/graphql/queries/account/authenticate.js new file mode 100644 index 0000000..0ec3543 --- /dev/null +++ b/src/graphql/queries/account/authenticate.js @@ -0,0 +1,14 @@ +import { asyncFancyFetch } from '../../../functions/fancy-fetches'; + +export default async function authenticate({ queryString, signal }) { + return await asyncFancyFetch({ + body: { + query: ` + query { + authenticate ${queryString} + } + ` + }, + signal + }); +} diff --git a/src/graphql/queries/blog/fetch-blog-post-by-ID.js b/src/graphql/queries/blog/fetch-blog-post-by-ID.js new file mode 100644 index 0000000..1ad3a20 --- /dev/null +++ b/src/graphql/queries/blog/fetch-blog-post-by-ID.js @@ -0,0 +1,15 @@ +import { asyncFancyFetch } from '../../../functions/fancy-fetches'; + +export default async function fetchBlogPostByID({ headers: { BlogPostID }, queryString, signal }) { + return await asyncFancyFetch({ + body: { + query: ` + query { + fetchBlogPostByID ${queryString} + } + ` + }, + headers: { BlogPostID }, + signal + }); +} diff --git a/src/graphql/queries/card/search-card.js b/src/graphql/queries/card/search-card.js new file mode 100644 index 0000000..54d933d --- /dev/null +++ b/src/graphql/queries/card/search-card.js @@ -0,0 +1,15 @@ +import { asyncFancyFetch } from '../../../functions/fancy-fetches'; + +export default async function searchCard({ queryString, signal, variables: { search } }) { + return await asyncFancyFetch({ + body: { + query: ` + query($search: String) { + searchCard (search: $search) ${queryString} + } + `, + variables: { search } + }, + signal + }); +} diff --git a/src/graphql/queries/card/search-printings.js b/src/graphql/queries/card/search-printings.js new file mode 100644 index 0000000..25e1816 --- /dev/null +++ b/src/graphql/queries/card/search-printings.js @@ -0,0 +1,15 @@ +import { asyncFancyFetch } from '../../../functions/fancy-fetches'; + +export default async function searchPrintings({ queryString, signal, variables: { oracle_id } }) { + return await asyncFancyFetch({ + body: { + query: ` + query($oracle_id: String) { + searchPrintings (oracle_id: $oracle_id) ${queryString} + } + `, + variables: { oracle_id } + }, + signal + }); +} diff --git a/src/graphql/queries/cube/fetch-cube-by-ID.js b/src/graphql/queries/cube/fetch-cube-by-ID.js new file mode 100644 index 0000000..9017268 --- /dev/null +++ b/src/graphql/queries/cube/fetch-cube-by-ID.js @@ -0,0 +1,15 @@ +import { asyncFancyFetch } from '../../../functions/fancy-fetches'; + +export default async function fetchCubeByID({ headers: { CubeID }, queryString, signal }) { + return await asyncFancyFetch({ + body: { + query: ` + query { + fetchCubeByID ${queryString} + } + ` + }, + headers: { CubeID }, + signal + }); +} diff --git a/src/graphql/queries/deck/fetch-deck-by-ID.js b/src/graphql/queries/deck/fetch-deck-by-ID.js new file mode 100644 index 0000000..fccbfc6 --- /dev/null +++ b/src/graphql/queries/deck/fetch-deck-by-ID.js @@ -0,0 +1,15 @@ +import { asyncFancyFetch } from '../../../functions/fancy-fetches'; + +export default async function fetchDeckByID({ headers: { DeckID }, queryString, signal }) { + return await asyncFancyFetch({ + body: { + query: ` + query { + fetchDeckByID ${queryString} + } + ` + }, + headers: { DeckID }, + signal + }); +} diff --git a/src/hooks/subscribe-hook.js b/src/hooks/subscribe-hook.js index 106881f..089b6c4 100644 --- a/src/hooks/subscribe-hook.js +++ b/src/hooks/subscribe-hook.js @@ -4,6 +4,7 @@ import Cookies from 'js-cookie'; export default function useSubscribe({ cleanup = () => null, + condition = true, connectionInfo = {}, dependencies = [], queryString = '', @@ -12,44 +13,50 @@ export default function useSubscribe({ update = () => null }) { useEffect(() => { - (async function () { - await setup(); - })(); + if (condition) { + (async function () { + await setup(); + })(); - const client = createClient({ - connectionParams: { - authToken: Cookies.get('authentication_token'), - ...connectionInfo - }, - url: process.env.REACT_APP_WS_URL - }); - - (async function () { - function onNext(message) { - update(message.data[subscriptionType]); - } + const client = createClient({ + connectionParams: { + authToken: Cookies.get('authentication_token'), + ...connectionInfo + }, + url: process.env.REACT_APP_WS_URL + }); - await new Promise((resolve, reject) => { - client.subscribe( - { - query: ` - subscription { - ${subscriptionType} {${queryString}} - } - ` - }, - { - complete: resolve, - error: reject, - next: onNext + (async function () { + function onNext(message) { + if (message.data) { + update(message.data[subscriptionType]); + } else { + console.log(message); } - ); - }); - })(); + } + + await new Promise((resolve, reject) => { + client.subscribe( + { + query: ` + subscription { + ${subscriptionType} ${queryString} + } + ` + }, + { + complete: resolve, + error: reject, + next: onNext + } + ); + }); + })(); - return () => { - cleanup(); - client.dispose(); - }; + return () => { + cleanup(); + client.dispose(); + }; + } }, [Cookies.get('authentication_token'), ...dependencies]); } diff --git a/src/pages/Account.jsx b/src/pages/Account.jsx index a4ab896..af0fc43 100644 --- a/src/pages/Account.jsx +++ b/src/pages/Account.jsx @@ -17,6 +17,7 @@ import MUITooltip from '@mui/material/Tooltip'; import MUITypography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; +import editAccount from '../graphql/mutations/account/edit-account'; import theme, { backgroundColor } from '../theme'; import Avatar from '../components/miscellaneous/Avatar'; import BudAccordion from '../components/Account Page/BudAccordion'; @@ -28,6 +29,7 @@ import ScryfallRequest from '../components/miscellaneous/ScryfallRequest'; import { AccountContext } from '../contexts/account-context'; import { AuthenticationContext } from '../contexts/Authentication'; import { PermissionsContext } from '../contexts/Permissions'; +import initiateBudRequest from '../graphql/mutations/account/initiate-bud-request'; const useStyles = makeStyles({ cardHeader: { @@ -48,21 +50,9 @@ const useStyles = makeStyles({ export default function Account() { const { accountID } = useParams(); + const { isLoggedIn, measurement_system, radius, userID } = useContext(AuthenticationContext); const { - isLoggedIn, - settings: { measurement_system, radius }, - userID - } = useContext(AuthenticationContext); - const { - accountState: { - avatar, - buds, - email, - name, - received_bud_requests, - sent_bud_requests - }, - editAccount, + accountState: { avatar, buds, email, name, received_bud_requests, sent_bud_requests }, setAccountState } = useContext(AccountContext); const { @@ -103,9 +93,7 @@ export default function Account() { label={ } @@ -131,18 +119,14 @@ export default function Account() { label={ } labelPlacement="start" /> {geolocationEnabled && ( -
    +
    Units @@ -151,14 +135,11 @@ export default function Account() { fullWidth label="Units" native - onChange={(event) => - editAccount( - `settings: { - measurement_system: ${event.target.value}, - radius: ${radius} - }` - ) - } + onChange={(event) => { + editAccount({ + variables: { measurement_system: event.target.value } + }); + }} value={measurement_system} inputProps={{ id: 'measurement-system-selector' @@ -168,25 +149,17 @@ export default function Account() { - - - Distance - + + Distance - editAccount( - `settings: { - measurement_system: ${measurement_system}, - radius: ${event.target.value} - }` - ) - } + onChange={(event) => { + editAccount({ + variables: { radius: event.target.value } + }); + }} value={radius} inputProps={{ id: 'radius-selector' @@ -208,34 +181,34 @@ export default function Account() { {isLoggedIn && accountID !== userID && !buds.find((bud) => bud._id === userID) && - !received_bud_requests.find( - (request) => request._id === userID - ) && - !sent_bud_requests.find( - (request) => request._id === userID - ) && ( + !received_bud_requests.find((request) => request._id === userID) && + !sent_bud_requests.find((request) => request._id === userID) && ( // only showing the add bud button if the user is logged in, they are viewing someone else's profile, and they are not already buds with nor have they already sent or received a bud request to or from the user whose profile they are viewing - editAccount( - `action: "send",\nother_user_id: "${accountID}",\nreturn_other: true` - ) - } + onClick={() => { + initiateBudRequest({ + variables: { other_user_id: accountID } + }); + }} > )} } - avatar={} + avatar={} className={classes.cardHeader} title={ editAccount(`name: "${event.target.value}"`) + onBlur: (event) => { + editAccount({ + variables: { name: event.target.value } + }); + } }} label="Account Name" onChange={(event) => { @@ -263,7 +236,9 @@ export default function Account() { buttonText="Change Avatar" labelText="Avatar" onSubmit={(chosenCard) => { - editAccount(`avatar: "${chosenCard.art_crop}"`); + editAccount({ + variables: { avatar: chosenCard._id } + }); }} /> diff --git a/src/pages/Blog.jsx b/src/pages/Blog.jsx index 0676296..424b1a5 100644 --- a/src/pages/Blog.jsx +++ b/src/pages/Blog.jsx @@ -43,7 +43,16 @@ export default function Blog() { _id author { _id - avatar + avatar { + card_faces { + image_uris { + art_crop + } + } + image_uris { + art_crop + } + } name } body @@ -97,8 +106,8 @@ export default function Blog() { toggleOpen={() => setBlogPostToDelete({ _id: null, title: null })} > - This action cannot be undone. Your wise counsel and witty humor will - be lost to the ages... + This action cannot be undone. Your wise counsel and witty humor will be lost to the + ages... @@ -113,9 +122,7 @@ export default function Blog() { }} > New Article - } + title={New Article} style={{ flexGrow: 1 }} subheader={ @@ -136,16 +143,7 @@ export default function Blog() { )} {blogPosts.map((blogPost) => ( - + - } + avatar={} subheader={ {blogPost.subtitle} } style={{ flexGrow: 1 }} - title={ - - {blogPost.title} - - } + title={{blogPost.title}} /> {blogPost.author._id === authentication.userID && ( diff --git a/src/pages/BlogPost.jsx b/src/pages/BlogPost.jsx index 0811064..687a18a 100644 --- a/src/pages/BlogPost.jsx +++ b/src/pages/BlogPost.jsx @@ -24,13 +24,14 @@ import { Link, useNavigate, useParams } from 'react-router-dom'; import AutoScrollMessages from '../components/miscellaneous/AutoScrollMessages'; import LoadingSpinner from '../components/miscellaneous/LoadingSpinner'; +import ScryfallRequest from '../components/miscellaneous/ScryfallRequest'; import createBlogPost from '../graphql/mutations/blog/create-blog-post'; +import createComment from '../graphql/mutations/blog/create-comment'; import editBlogPost from '../graphql/mutations/blog/edit-blog-post'; import theme, { backgroundColor } from '../theme'; import { AuthenticationContext } from '../contexts/Authentication'; import { BlogPostContext } from '../contexts/blog-post-context'; import { ErrorContext } from '../contexts/Error'; -import ScryfallRequest from '../components/miscellaneous/ScryfallRequest'; const useStyles = makeStyles({ article: { @@ -99,24 +100,11 @@ const useStyles = makeStyles({ export default function BlogPost() { const { userID } = useContext(AuthenticationContext); const { - loading, - blogPostState: { - author, - body, - comments, - image, - published, - subtitle, - title, - updatedAt - }, - createComment, + blogPostState: { author, body, comments, image, published, subtitle, title, updatedAt }, setBlogPostState } = useContext(BlogPostContext); const { setErrorMessages } = useContext(ErrorContext); - const blogPostImageWidth = useMediaQuery(theme.breakpoints.up('md')) - ? 150 - : 75; + const blogPostImageWidth = useMediaQuery(theme.breakpoints.up('md')) ? 150 : 75; const navigate = useNavigate(); const { blogPostID } = useParams(); const [editing, setEditing] = useState(blogPostID === 'new-post'); @@ -124,9 +112,7 @@ export default function BlogPost() { const [success, setSuccess] = useState(false); const { article } = useStyles(); - return loading ? ( - - ) : ( + return ( - A work of genius by:{' '} - {author.name} + A work of genius by: {author.name} - {`Last updated ${new Date( - parseInt(updatedAt) - ).toLocaleString()}.`} + {`Last updated ${new Date(parseInt(updatedAt)).toLocaleString()}.`} } @@ -205,13 +188,7 @@ export default function BlogPost() { onClick={() => { setEditing((prevState) => !prevState); }} - startIcon={ - editing ? ( - - ) : ( - - ) - } + startIcon={editing ? : } > {editing ? 'Preview' : 'Edit'} @@ -263,13 +240,15 @@ export default function BlogPost() { )}\n\n
    \n${
                         cardData.name.replace('//', '/').split('/')[0]
                       }\n
    ***Insert amazing commentary here***
    \n
    \n\n${ - cardData.back_image + !cardData.image_uris ? `
    \n${
                               cardData.name.replace('//', '/').split('/')[1]
                             }\n
    ***Insert amazing commentary here***
    \n
    \n` : '' }\n\n
    \n\n` @@ -299,9 +278,7 @@ export default function BlogPost() { {subtitle}
    - - {body} - + {body}
    )} @@ -326,22 +303,14 @@ export default function BlogPost() { navigate('/blog'); }, 1000); } catch (error) { - setErrorMessages((prevState) => [ - ...prevState, - error.message - ]); + setErrorMessages((prevState) => [...prevState, error.message]); } finally { setPosting(false); } }} startIcon={(() => { if (posting) { - return ( - - ); + return ; } if (success) { return ; @@ -370,22 +339,14 @@ export default function BlogPost() { navigate('/blog'); }, 1000); } catch (error) { - setErrorMessages((prevState) => [ - ...prevState, - error.message - ]); + setErrorMessages((prevState) => [...prevState, error.message]); } finally { setPosting(false); } }} startIcon={(() => { if (posting) { - return ( - - ); + return ; } if (success) { return ; @@ -403,7 +364,9 @@ export default function BlogPost() { {blogPostID !== 'new-post' && ( createComment(value)} + submitFunction={(value) => + createComment({ headers: { BlogPostID: blogPostID }, variables: { body: value } }) + } title="Community Reaction" /> )} diff --git a/src/pages/Classy.jsx b/src/pages/Classy.jsx index 850ef42..cb06a44 100644 --- a/src/pages/Classy.jsx +++ b/src/pages/Classy.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import MUIArrowRightIcon from '@mui/icons-material/ArrowRight'; import MUICard from '@mui/material/Card'; @@ -11,7 +11,9 @@ import MUIListItemText from '@mui/material/ListItemText'; import MUITypography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; +import classyBannedList from '../constants/classy-banned-list'; import theme from '../theme'; +import ScryfallCardLink from '../components/miscellaneous/ScryfallCardLink'; const useStyles = makeStyles({ multiColumnList: { @@ -34,171 +36,8 @@ const useStyles = makeStyles({ }); export default function Classy() { - const bannedCards = [ - "Accorder's Shield", - 'Allosaurus Rider', - 'Alpine Meadow', - 'Ancestral Vision', - 'Arctic Flats', - 'Arctic Treeline', - 'Asmoranomardicadaistinaculdacar', - 'Birthing Pod', - 'Blood Moon', - 'Boil', - 'Boiling Seas', - 'Bone Saw', - 'Boreal Shelf', - 'Bridge From Below', - "Cathar's Shield", - 'Choke', - 'Chrome Mox', - 'Claws of Gix', - 'Cloudpost', - 'Commandeer', - 'Crashing Footfalls', - 'Dakmor Salvage', - 'Dark Depths', - 'Darkblast ', - 'Darksteel Relic', - 'Dig Through Time', - 'Disrupting Shoal', - "Dragon's Rage Channeler", - 'Endurance', - 'Ensnaring Bridge', - 'Evermind', - 'Eye of Ugin', - 'Faceless Haven', - 'Faithless Looting', - 'Field of the Dead', - 'Flashfires', - 'Force of Despair', - 'Force of Negation', - 'Force of Rage', - 'Force of Vigor', - 'Force of Virtue', - 'Fountain of Youth', - 'Frost Marsh', - 'Frostwalk Bastion', - 'Fury', - 'Fury of the Horde', - "Gaea's Will", - 'Gitaxian Probe', - 'Glacial Floodplain', - 'Glimpse of Nature', - 'Glimpse of Tomorrow', - 'Golgari Brownscale', - 'Golgari Grave Troll', - 'Golgari Thug', - 'Grave-Shell Scarab', - 'Greater Mossdog', - 'Grief', - 'Gyruda, Doom of Depths', - 'Hangarback Walker', - 'Herbal Poultice', - 'Highland Forest', - 'Highland Weald', - 'Hogaak, Arisen Necropolis', - 'Hypergenesis', - 'Ice Tunnel', - 'Inevitable Betrayal', - 'Intervention Pact', - 'Jegantha, the Wellspring', - 'Kaheera, the Orphanguard', - 'Keruga, the Macrosage', - 'Kite Shield', - 'Krark-Clan Ironworks', - 'Leyline of Abundance', - 'Leyline of Anticipation', - 'Leyline of Combustion', - 'Leyline of Lifeforce', - 'Leyline of Lightning', - 'Leyline of Punishment', - 'Leyline of Sanctity', - 'Leyline of Singularity', - 'Leyline of the Meek', - 'Leyline of the Void', - 'Leyline of Vitality', - 'Life from the Loam', - 'Living End', - 'Lotus Bloom', - 'Lurrus of the Dream-Den', - 'Lutri, the Spellchaser', - 'Marrow Shards', - 'Memnite', - 'Mental Misstep', - 'Mine Collapse', - "Mishra's Bauble", - 'Moldervine Cloak', - 'Mouth of Ronom', - 'Mox Amber', - 'Mox Opal', - 'Mox Tantalite', - 'Mutagenic Growth', - 'Mycosynth Lattice', - 'Necroplasm', - 'Nightmare Void', - 'Nourishing Shoal', - 'Noxious Revival', - 'Obosh, the Preypiercer', - 'Oko, Thief of Crowns', - 'Once Upon a Time', - 'Ornithopter', - 'Pact of Negation', - 'Pact of the Titan', - 'Paradise Mantle', - 'Profane Tutor', - 'Ragavan, Nimble Pilferer', - 'Restore Balance', - 'Resurgent Belief', - 'Rimewood Falls', - 'Rite of Flame', - 'Scrying Sheets', - "Sensei's Divining Top", - 'Shambling Shell', - 'Shenanigans', - 'Shimmerdrift Vale', - 'Shining Shoal', - 'Sickening Shoal', - 'Simian Spirit Guide', - 'Skullclamp', - 'Slaughter Pact', - 'Snapback', - 'Snow-Covered Forest', - 'Snow-Covered Island', - 'Snow-Covered Mountain', - 'Snow-Covered Plains', - 'Snow-Covered Swamp', - 'Snowfield Sinkhole', - 'Sol Talisman', - 'Solitude', - 'Soul Spike', - 'Spellbook', - 'Spidersilk Net', - 'Stinkweed Imp', - 'Subtlety', - 'Sulfurous Mire', - 'Summer Bloom', - "Summoner's Pact", - 'Sunscour', - 'Surgical Extraction', - "Tibalt's Trickery", - "Tormod's Crypt", - 'Tresserhorn Sinks', - "Umezawa's Jitte", - 'Umori, the Collector', - "Uro, Titan of Nature's Wrath", - "Urza's Saga", - 'Veil of Summer', - 'Volatile Fjord', - 'Walking Ballista', - 'Welding Jar', - 'Wheel of Fate', - 'Woodland Chasm', - 'Worship', - 'Yorion, Sky Nomad', - 'Zirda, the Dawnwaker', - 'Zuran Orb' - ]; + const { hash } = useLocation(); + const [classyBannedListState, setClassyBannedListState] = useState(classyBannedList); const classes = useStyles(); const sections = [ { @@ -208,128 +47,60 @@ export default function Classy() { No "Gotcha" Cards. - - Some cards either do nothing at all if your opponent has an answer - (typically an answer which is pretty narrow and that decks often - can't afford to mainbaord), or effectively win the game by radically - shifting Magic's goal posts at a mana cost that is far too cheap and - only ask the player to satisfy a very easy condition.{' '} - - Ensnaring Bridge - {' '} - is the quintessential example. Such cards are NOT classy. + + { + "Some cards either do nothing at all if your opponent has an answer (typically an answer which is pretty narrow and that decks often can't afford to mainbaord), or effectively win the game by radically shifting Magic's goal posts at a mana cost that is far too cheap and only ask the player to satisfy a very easy condition. " + } + + {' is the quintessential example. Such cards are NOT classy.'} - - "Gotcha" cards typically reduce the outcome of a game of Magic to a - few yes/no questions. Did I draw the card and have enough mana to - cast it? Did my opponent draw their answer and were they able to - cast it? There is often little or no meaningful interaction or - interesting decisions to be made in those games. Players are not - rewarded or punished for decisions they make regarding sequencing, - attacking, blocking, bluffing, exchanging resources, managing life - totals and tempo. Instead, players are punished for playing Magic - the way they are supposed to, rewarded for building decks that seek - to force the game to be played on a completely unrelated axis, and - pretty much just win or lose based on the luck of the draw. If - you're into that kind of thing, Modern is a great format! + + { + '"Gotcha" cards typically reduce the outcome of a game of Magic to a few yes/no questions. Did I draw the card and have enough mana to cast it? Did my opponent draw their answer and were they able to cast it? There is often little or no meaningful interaction or interesting decisions to be made in those games. Players are not rewarded or punished for decisions they make regarding sequencing, attacking, blocking, bluffing, exchanging resources, managing life totals and tempo. Instead, players are punished for playing Magic the way they are supposed to, rewarded for building decks that seek to force the game to be played on a completely unrelated axis, and pretty much just win or lose based on the luck of the draw. If you\'re into that kind of thing, Modern is a great format!' + } No Free Spells. - - In order to help keep this format fair enough that it can be a - brewer's paradise, free spells (non-land cards that do not require a - mana investment in order to cast) are not allowed. This includes 0 - mana cost artifacts like{' '} - - Mishra's Bauble - {' '} - and{' '} - - Memnite - - , cards which can be cast for only Phyrexian mana such as{' '} - - Gut Shot - {' '} - and{' '} - - Surgical Extraction - {' '} - as well as the free spell{' '} - - Manamorphose - - . These spells are NOT classy. + + { + "In order to help keep this format fair enough that it can be a brewer's paradise, free spells (non-land cards that do not require a mana investment in order to cast) are not allowed. This includes 0 mana cost artifacts like " + } + + {' and '} + + {', cards which can be cast for only Phyrexian mana such as '} + + {' and '} + + {' as well as the free spell '} + + {'. These spells are NOT classy.'} - - Cards whose mana cost can be reduced to 0 by satisfying certain - conditions, such as{' '} - + + {'Cards whose mana cost can be reduced to 0 by satisfying certain conditions, such as '} + Frogmite - , or paying alternative costs such as{' '} + {', or paying alternative costs such as '} Ephemeral Shields - {' '} - are allowed, so long as those alternative costs typically require a - mana investment (it is possible your opponent cast{' '} + + { + ' are allowed, so long as those alternative costs typically require a mana investment (it is possible your opponent cast ' + } Necromentia - {' '} - leaving you with 3 zombie tokens which you could use to cast{' '} + + {' leaving you with 3 zombie tokens which you could use to cast '} Demon of Death's Gate - , but since there are no free spells allowed in the Classy format, - you generally would have spent mana on the three creatures you are - sacrificing). Therefore, the "Force of" cycle from{' '} - + { + ', but since there are no free spells allowed in the Classy format, you generally would have spent mana on the three creatures you are sacrificing). Therefore, the "Force of" cycle from ' + } + Modern Horizons - {' '} - and the "Evoke Elemental" cycle from{' '} - + + {' and the "Evoke Elemental" cycle from '} + Modern Horizons 2 - {' '} - are NOT classy because their alternative costs require the exiling - of cards from your hand, which means, in all likelihood, no mana has - yet been invested in them. + + { + ' are NOT classy because their alternative costs require the exiling of cards from your hand, which means, in all likelihood, no mana has yet been invested in them.' + } - - Another gray area concerns cards which can recur from the graveyard - without paying any mana, such as{' '} - + + { + 'Another gray area concerns cards which can recur from the graveyard without paying any mana, such as ' + } + Bloodghast - . When graveyard enablers have been too good, modern has been - dominated by fast, unfair graveyard decks such as dredge and Izzet - phoenix. Since the banning of{' '} - - Faithless Looting - - , these decks have completely fallen out of favor. That suggests - recursive creatures are often not the problem, but rather cards - which too easily enable players to abuse their graveyards. - Extracting value from the graveyard is something many decks seek to - do and can really make you feel like a naughty boy. It is not my - desire to preclude players from playing such cards; only that all - decks seek to play by the "normal" rules of the game (casting - spells, playing to the board, etc...). Recursive cards are classy. + { + '. When graveyard enablers have been too good, modern has been dominated by fast, unfair graveyard decks such as dredge and Izzet phoenix. Since the banning of ' + } + + { + ', these decks have completely fallen out of favor. That suggests recursive creatures are often not the problem, but rather cards which too easily enable players to abuse their graveyards. Extracting value from the graveyard is something many decks seek to do and can really make you feel like a naughty boy. It is not my desire to preclude players from playing such cards; only that all decks seek to play by the "normal" rules of the game (casting spells, playing to the board, etc...). Recursive cards are classy.' + } No Companions. - - If I wanted to play commander, which I don't, then I'd play - commander. Companions are NOT classy. + + { + "If I wanted to play commander, which I don't, then I'd play commander. Companions are NOT classy." + } No Tier 0 Cards. - - This restriction is admittedly a bit subjective. I would describe a - tier 0 card as one which is provides so much value at it's - color/CMC/card type and asks so little of you in terms of deck - building concessions that all decks playing that color and archetype - (by which I mean aggro, combo, control and midrange) either play the - card or are self-handicapping by not playing it. For a tier 0 card, - there are often many cards which are comparable in function and are - good enough that they would see play if the tier 0 card was not - legal. When tier 0 cards are allowed to exist in a format, their - alternatives are largely precluded from serious consideration which - means the pool of viable cards which brewers have to work with is - smaller than it should be. + + { + "This restriction is admittedly a bit subjective. I would describe a tier 0 card as one which is provides so much value at it's color/CMC/card type and asks so little of you in terms of deck building concessions that all decks playing that color and archetype (by which I mean aggro, combo, control and midrange) either play the card or are self-handicapping by not playing it. For a tier 0 card, there are often many cards which are comparable in function and are good enough that they would see play if the tier 0 card was not legal. When tier 0 cards are allowed to exist in a format, their alternatives are largely precluded from serious consideration which means the pool of viable cards which brewers have to work with is smaller than it should be." + } - - Tier 0 cards are creatures more often than not. For example, in - Modern,{' '} - + + { + 'Tier 0 cards, in more moder Magic sets, are permanents more often than not. For example, in Modern, ' + } + Opt - {' '} - is the most ubiquitous one mana blue cantrip, but I don't believe - that makes it tier 0. There simply aren't many options for this sort - of effect. To be exact, as of writing this (August 2021), there are - 14 one mana blue instants legal in Modern that draw a card, and Opt - is the only one that offers card selection. If Opt wasn't legal, I - don't think decks would start playing{' '} + + { + "is the most ubiquitous one mana blue cantrip, but I don't believe that makes it tier 0. There simply aren't many options for this sort of effect. To be exact, as of writing this (August 2021), there are 14 one mana blue instants legal in Modern that draw a card, and Opt is the only one that offers card selection. If Opt wasn't legal, I don't think decks would start playing " + } Fleeting Distraction - {' '} - or{' '} + + {' or '} Whispers of the Muse - . Those cards just aren't good enough for Modern.{' '} + {". Those cards just aren't good enough for Modern. "} Thought Scour - {' '} - and{' '} + + {' and '} Visions of Beyond - {' '} - do see play in more niche decks, but the bottom line is that Opt - just doesn't have a lot of competition. It is not a busted card that - feels miserable to have played against you. It is a reasonably - costed card for the effect it provides. Decks that are interested in - that sort of effect will play it (at least until Midnight Hunt comes - out, and then{' '} - + + { + " do see play in more niche decks, but the bottom line is that Opt just doesn't have a lot of competition. It is not a busted card that feels miserable to have played against you. It is a reasonably costed card for the effect it provides. Decks that are interested in that sort of effect will play it (at least until Midnight Hunt comes out, and then " + } + Consider - {' '} - will probably replace it in most decks), but decks don't go out of - their way to include it. People don't splash blue just so they can - run Opt, which, when that is happening, is another indication of a - tier 0 card (such as when all sorts of decks were splashing green in - order to play{' '} - - Oko, Thief of Crowns - {' '} - before it was finally banned). + + { + " will probably replace it in most decks), but decks don't go out of their way to include it. People don't splash blue just so they can run Opt, which, when that is happening, is another indication of a tier 0 card (such as when all sorts of decks were dipping into blue and green in order to play " + } + + {' before it was finally banned).'} - - Next let's consider{' '} - - Dragon's Rage Channeler - {' '} - (DRC), a new card from Modern Horizons 2. For a single red mana, you - get a 1/1 body which lets you surveil any time you cast a - non-creature spell. And if you have 4 or more card types in your - graveyard, it gets +2/+2, flying and must attack each combat if - able. After MH2 released, DRC became an instant staple of blue-red, - Grixis and Rakdos decks and is also seeing play in Jeskai and Jund - builds as well. These decks were all very solid before DRC, most at - or near the top of the metagame. In terms of it's body, as soon as - it was spoiled, it was drawing comparisons to{' '} + + {"Next let's consider "} + + { + " (DRC), a new card from Modern Horizons 2. For a single red mana, you get a 1/1 body which lets you surveil any time you cast a non-creature spell. And if you have 4 or more card types in your graveyard, it gets +2/+2, flying and must attack each combat if able. After MH2 released, DRC became an instant staple of blue-red, Grixis and Rakdos decks and is also seeing play in Jeskai and Jund builds as well. These decks were all very solid before DRC, most at or near the top of the metagame. In terms of it's body, as soon as it was spoiled, it was drawing comparisons to " + } Delver of Secrets - , which only 3 years ago was named by ChannelFireball the 46th best - Magic card of all time, and the 7th best creature (you can watch the - videos{' '} + { + ', which only 3 years ago was named by ChannelFireball the 46th best Magic card of all time, and the 7th best creature (you can watch the videos ' + } here - ). DRC blows Delver out of the water. It offers you card selection - on every non-creature spell you cast and, unless your opponent has - out{' '} + { + '). DRC blows Delver out of the water. It offers you card selection on every non-creature spell you cast and, unless your opponent has out ' + } Rest in Peace - {' '} - or something, can more reliably be "turned on" than Delver. At it's - core, DRC is a cheap, aggressive, red creature, of which there are - dozens only a step behind DRC in terms of their ability to damage - your opponent. Several of them were good enough to see significant - Modern play right up until DRC became legal and I am positive that - should Wizards of the Coast ever admit DRC was too pushed and ban - it, those cards would immediately see, not just play, but - considerable success, once again. So to recap, not only is DRC the - best in its class at reliably getting in for damage, it also offers - superb card selection, which is huge because one of the ways - aggressive decks can lose is by drawing too many lands, and card - selection is something which no other red one drop offers at all - (even in the inferior version of surveil, scry), AND it helps decks - abuse the graveyard by freely putting cards there from your library. - The card does so much more than just about every other red one drop, - with the exception of another card introduced in Modern Horizons 2 ( - - Ragavan, Nimble Pilferer - ). Yes, it does die to removal but so do all other red one drops. - And it is hard to make the argument that a one drop which must be - answered quickly or else it will provide loads of value is a - reasonable and fair Magic card. DRC simply does too much, which - makes deck building and brewing less interesting. It is to - aggressive decks what{' '} - - Uro, Titan of Nature's Wrath - {' '} - was to control decks before it was finally banned. Cards like this - homogenize decks and are NOT classy. + { + ' or something, and can more reliably be "turned on" than Delver. Also, despite being a very powerful card, Delver of Secrets has never been the powerhouse in modern that it is in Legacy since ' + } + + Brainstorm + + { + ' is not legal in the format, meaning a turn 2 Insectile Aberration is far less likely. Blue decks are also not generally about winning by attacking with french-vanilla creatures. It would almost be like if you gave green access to a card like ' + } + + Serum Visions + + { + " or something. Green's strength is in mana ramping and playing to the board, so even though green players have access to some decent cantrips, they don't see nearly as much play as blue cantrips. Spending mana to exchange a card in your hand for something else means you didn't spend that mana playing to the board. Although there are some green based combo decks like certain Elf builds and " + } + + Primeval Titan + + { + " decks, typical green decks are more about overwhelming the opponent with board presence and resources and less about assembling specific pieces for a combo finish. Green doesn't get many payoffs for having instant and sorcery cards in their library. Similarly, you could argue that Delver of Secrets is less powerful than it would be if it was red, a color whose supporting cards and normal gameplan would more naturally be interested in such a cheap, evasive creature with high power for its mana cost and which requires a decent portion of your deck to be instant and sorcery spells in order to work. Despite all of these balancing factors though, Delver has seen some modest modern play at various points. Even today, it is playable. Not tier 1, but it is good enough that if you wanted to play it, you could and still have some success." + } + + + { + "At it's core, DRC is a cheap, aggressive, red creature, of which there are several only a half step behind DRC in terms of their ability to quickly damage your opponent. Several of them were good enough to see significant Modern play right up until DRC was released and I am positive that should Wizards of the Coast ever admit DRC was too pushed and ban it, those cards would immediately see, not just play, but considerable success, once again. So to recap, not only is DRC the best in its class at reliably getting in for damage, it also offers superb card selection, which is huge because one of the ways aggressive decks can lose is by casting all of their cheap, relatively less impactful spells, and then stalling out by drawing too many lands, at which point an opponent with a more balanced mana curve has an opportunity to turn the corner. Card selection is also something which no other red 1 CMC creature offers at all (even in the inferior version of surveil, scry). It also helps decks abuse the graveyard, traditionally something very powerful in eternal formats, by freely putting cards there from your library. The card does so much more than just about every other red one drop, with the exception of a single other card also introduced in Modern Horizons 2 (" + } + + { + '). Yes, they both die to removal but so do all other red one drops. And it is hard to make the argument that a one drop which must be answered almost immediately or it will provide a nearly insurmountable amount of value is a reasonable and fair Magic card. DRC simply does too much, which makes deck building and brewing less interesting. It is to aggressive decks what ' + } + + { + ' was to control decks before it was finally banned. Cards like these are so good that you are either forced to include them or you are handicapping yourself. They give players fewer viable options, homogenize decks and are NOT classy.' + } No Snow. - - I have always thought snow was so lame. Other than the fact that two - cards now exist ( + + { + 'I have always thought snow was so lame. Other than the fact that two cards now exist (' + } Break the Ice - {' '} - and{' '} + + {' and '} Reidane, God of the Worthy - ), which don't even see any play because they are pretty narrow and - not even that strong as hate cards, snow basic lands are just strict - upgrades to basic lands. They function the exact same way; they just - have the word "snow" on them. So although there is essentially no - downside to playing snow basics, there is upside as you get access - to cards like{' '} + { + '), which don\'t even see any play because they are pretty narrow and not even that good as hate cards, snow basic lands are just strict upgrades to basic lands. They function the exact same way; they just have the word "snow" on them. So although there is essentially no downside to playing snow basics, there is upside as you get access to cards like ' + } Blood on the Snow - . There are also only a few different printings of snow basics, - which means your choices for art are limited. Which is a shame - because there is so much cool artwork on basic lands. My format, my - rules; snow is NOT classy. + { + '. There are also only a few different printings of snow basics, which means your choices for art are limited. Which is a shame because there is so much cool artwork on basic lands. My format, my rules; snow is NOT classy.' + } No Dredge. - - Dredge is just a super busted mechanic. It is a 10 on Mark - Rosewater's storm scale (you can read more about that{' '} - + + { + "Dredge is just a super busted mechanic. It is a 10 on Mark Rosewater's storm scale (you can read more about that " + } + here - ), which basically means Wizards of the Coast knows that it was a - mistake to ever print. If you look at a dredge deck, cards with the - dredge mechanic are terrible Magic cards, but the fact that they - have the dredge mechanic makes them busted as graveyard enablers. - Dredge decks do not seek to play normal games of Magic or even cast - many of the spells in their deck. Dredge cards are NOT classy. + { + '), which basically means Wizards of the Coast knows that it was a mistake to ever print. If you look at a dredge deck, cards with the dredge mechanic are terrible Magic cards, but the fact that they have the dredge mechanic makes them busted as graveyard enablers. Dredge decks do not seek to cast many of the spells in their deck. Dredge cards are NOT classy.' + } ), @@ -664,16 +331,10 @@ export default function Classy() { { id: 'card-legality', info: ( - - All cards which have been printed into a standard-legal set beginning - with Eighth Edition, or which have been printed into a Modern Horizons - set, and which are not on the Classy banned list, enumerated below, - are classy. In other words, all cards from sets which are legal in the - Modern format, with the exception of banned cards, are legal. + + { + 'All cards which have been printed into a standard-legal set beginning with Eighth Edition, or which have been printed into a Modern Horizons set, and which are not on the Classy banned list, enumerated below, are classy. In other words, all cards from sets which are legal in the Modern format, with the exception of banned cards, are legal.' + } ), title: 'Card Legality' @@ -682,49 +343,33 @@ export default function Classy() { id: 'banned-list', info: ( - - In order to maintain a fair, interesting and balanced environment, - in line with the ideals of the Classy format, a banned list is - necessary. This banned list is independent of the banned list for - the Modern format. Notable cards which are currently banned in - Modern but are legal in Classy are{' '} + + { + 'In order to maintain a fair, interesting and balanced environment, in line with the ideals of the Classy format, a banned list is necessary. This banned list is independent of the banned list for the Modern format and will never be managed by Wizards of the Coast. Notable cards which are currently banned in Modern but are legal in Classy are ' + } Splinter Twin - {' '} - and{' '} - - Preordain - . Keep in mind that many of these cards are on the list not for - power level concerns, but for violating one of the hard rules of the - format, such as{' '} - - Snow-Covered Forest + {' and '} + + Preordain - . The following cards are NOT classy: + { + '. Keep in mind that many of these cards are on the list not for power level concerns, but for violating one of the hard rules of the format, such as ' + } + + {'. The following cards are NOT classy:'} - {bannedCards.map((card, index) => ( - + {Object.entries(classyBannedList).map(([key, value], index) => ( + - {index + 1}) {card} + {index + 1}) @@ -738,45 +383,20 @@ export default function Classy() { id: 'deck-size-and-copy-limit', info: ( - - Because this is a classy format, a deck must contain exactly 69 - cards in the mainboard and can contain up to 21 cards in the - sideboard. The increased deck size is intended to decrease the odds - of games playing out in similar ways over and over again. The - increased sideboard size acknowledges that Magic decks do powerful - and diverse things and allowing players access to more cards to try - to better match up after game 1 should lead to fewer non-games. + + { + 'Being the Classy format, a deck must contain exactly 69 cards in the mainboard and can contain up to 21 cards in the sideboard. The increased deck size is intended to decrease the odds of games playing out in similar ways over and over again. The increased sideboard size acknowledges that Magic decks do powerful and diverse things so allowing players access to more cards to try to better match up in games 2 and 3 should lead to fewer non-games and, hopefully, the format not being dominated by decks doing very obscure things.' + } - - 69 and 21 (and therefore 90 as well) are not cleanly divisible by 4. - So aesthetically speaking, limiting copies to 3 feels more - satisfying. This also will require players to experiment with and - include cards that wouldn't make the cut if there was a 4 copy - limit. A 3 copy limit still allows decks to have some of the - redundancy that they rely on, but a bit less of it. So, for all - NON-LEGENDARY, NON-LAND cards, the limit is 3 copies. + + { + '69 and 21 (and therefore 90 as well) are not cleanly divisible by 4. So aesthetically, limiting copies to 3 feels more satisfying. This also will require players to experiment with and include pet cards that might not make the cut in tier 1 competitive lists if there was a 4 copy limit. A 3 copy limit still allows decks to have some of the redundancy that they rely on, but a bit less of it. So, for all NON-LEGENDARY, NON-LAND cards, the limit is 3 copies.' + } - - For LEGENDARY cards and NON-BASIC lands, the limit is 1 copy. This - is much more sensible from a flavor point of view. It can be very - frustrating to sink resources into dealing with a powerful legendary - creature or planeswalker only to have your opponent play a second - copy. This should help legendary cards feel more special when they - are played since they won't be drawn in most games. Although some - non-basic lands are legendary, and others, based on their name, - should have been given the legendary supertype (for example,{' '} + + { + "For LEGENDARY cards and NON-BASIC lands, the limit is 1 copy. This is much more sensible from a flavor point of view. It can be very frustrating to sink resources into dealing with a powerful legendary creature or planeswalker only to have your opponent play a second copy. This should help legendary cards feel more special when they are played since they won't be drawn in most games. Although some non-basic lands are legendary, others, based on their name, should have been given the legendary supertype (for example, " + } {' '} Molten Pinnacle - , emphasis added), limiting non-basic lands to a single copy is - primarily about keeping mana bases modest and making players work a - little harder to get delerium or to delve away a bunch of cards. - There are a lot of respectable non-basics out there; it has always - seemed a bit of a shame to me that so many of them just never really - see much play because fetches and shocks are typically the best - options, but only by a couple of percentage points. For BASIC lands, - the limit is 90 copies in a deck. + { + ', emphasis added). Limiting non-basic lands to a single copy is primarily about keeping mana bases modest, decks a bit more affordable, forcing players to get a bit more creative in finding synergies between their land base and their spells and making them work a little harder to get delerium or to delve away a bunch of cards. There are a lot of respectable non-basics out there; it has always seemed a bit of a shame to me that so many of them just never really see much play because fetches and shocks are typically the best options, but only by a couple of percentage points. For BASIC lands, the limit is 90 copies in a deck.' + } ), @@ -803,9 +418,68 @@ export default function Classy() { } ]; - const { hash } = useLocation(); + useEffect(() => { + (async () => { + const bannedCardsIdentifierArray = Object.values(classyBannedList).map(({ name }) => ({ + name + })); + const numberOfScryfallRequests = Math.ceil(bannedCardsIdentifierArray.length / 75); + const scryfallRequestArrays = []; + + for (let requestNumber = 0; requestNumber < numberOfScryfallRequests; requestNumber++) { + scryfallRequestArrays.push(bannedCardsIdentifierArray.splice(0, 75)); + } + + const populatedObject = {}; + + for (const request of scryfallRequestArrays) { + const rawScryfallResponse = await fetch('https://api.scryfall.com/cards/collection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ identifiers: request }) + }); + const parsedScryfallResponse = await rawScryfallResponse.json(); + + for (const card of parsedScryfallResponse.data) { + const { layout, name, oracle_id, scryfall_uri } = card; + let back_image, image; + switch (layout) { + case 'meld': + const meldResult = await fetch( + card.all_parts.find((part) => part.component === 'meld_result').uri + ).json(); + back_image = meldResult.image_uris.large; + image = card.image_uris.large; + break; + case 'modal_dfc': + back_image = card.card_faces[1].image_uris.large; + image = card.card_faces[0].image_uris.large; + break; + case 'transform': + back_image = card.card_faces[1].image_uris.large; + image = card.card_faces[0].image_uris.large; + break; + default: + back_image = null; + image = card.image_uris.large; + } + populatedObject[name] = { + back_image, + image, + name, + oracle_id, + scryfall_uri + }; + } + } + + setClassyBannedListState(populatedObject); + })(); + }); - React.useEffect(() => { + useEffect(() => { if (hash === '') { window.scrollTo(0, 0); } else { @@ -820,44 +494,40 @@ export default function Classy() { }, [hash]); return ( - - - Classy} - subheader="The hot new MTG format the cool kids can't get enough of" - /> - - - I'm a big fan of the Modern format, but there are aspects of it that - I have always hated. Since I've got my own website, I invented - Classy. - - - {sections.map((section) => ( - - - - - - {section.title} - - - ))} - - -
    - {sections.map((section) => ( - + Object.values(classyBannedListState).some(({ scryfall_uri }) => !!scryfall_uri) && ( + + {section.title}
    } + title={Classy} + subheader="The hot new MTG format the cool kids can't get enough of" /> - {section.info} + + + { + "I'm a big fan of the Modern format, but there are aspects of it that I have always hated. Since I've got my own website, I invented Classy." + } + + + {sections.map((section) => ( + + + + + + {section.title} + + + ))} + + - ))} - + {sections.map((section) => ( + + {section.title}} /> + {section.info} + + ))} + + ) ); } diff --git a/src/pages/Cube.jsx b/src/pages/Cube.jsx index 99baa28..c156a60 100644 --- a/src/pages/Cube.jsx +++ b/src/pages/Cube.jsx @@ -24,11 +24,7 @@ export default function Cube() { return ( {selectedCard && ( - setSelectedCard()} - editable={editable} - /> + setSelectedCard()} editable={editable} /> )} @@ -48,7 +44,7 @@ export default function Cube() { headers: { CubeID: cubeID }, variables: { componentID: activeComponentState._id, - scryfall_id: cardData.scryfall_id + scryfall_id: cardData._id } }) } diff --git a/src/pages/Deck.jsx b/src/pages/Deck.jsx index 7b3f0ef..cf4f4d6 100644 --- a/src/pages/Deck.jsx +++ b/src/pages/Deck.jsx @@ -1,58 +1,14 @@ -import React, { useContext } from 'react'; -import MUIPaper from '@mui/material/Paper'; +import React from 'react'; -import BasicLandAdder from '../components/miscellaneous/BasicLandAdder'; -import DeckDisplay from '../components/miscellaneous/DeckDisplay'; +import DeckDisplay from '../components/Deck Page/DeckDisplay'; import DeckInfo from '../components/Deck Page/DeckInfo'; -import LoadingSpinner from '../components/miscellaneous/LoadingSpinner'; -import ScryfallRequest from '../components/miscellaneous/ScryfallRequest'; -import { AuthenticationContext } from '../contexts/Authentication'; -import { DeckContext } from '../contexts/deck-context'; export default function Deck() { - const { userID } = useContext(AuthenticationContext); - const { - loading, - deckState: { creator, image, mainboard, name, sideboard }, - addCardsToDeck, - removeCardsFromDeck, - toggleMainboardSideboardDeck - } = useContext(DeckContext); - - return loading ? ( - - ) : ( + return ( - {creator._id === userID && ( - - - addCardsToDeck(cardData, 'mainboard', 1) - } - /> - - addCardsToDeck(cardData, 'mainboard', 1)} - /> - - - )} - - + ); } diff --git a/src/pages/Event.jsx b/src/pages/Event.jsx index 44be21e..0f9c1c1 100644 --- a/src/pages/Event.jsx +++ b/src/pages/Event.jsx @@ -10,7 +10,7 @@ import AutoScrollMessages from '../components/miscellaneous/AutoScrollMessages'; import BasicLandAdder from '../components/miscellaneous/BasicLandAdder'; import CardPoolDownloadLinks from '../components/Event Page/CardPoolDownloadLinks'; import ConfirmationDialog from '../components/miscellaneous/ConfirmationDialog'; -import DeckDisplay from '../components/miscellaneous/DeckDisplay'; +// import DeckDisplay from '../components/miscellaneous/DeckDisplay'; import EventInfo from '../components/Event Page/EventInfo'; import { EventContext } from '../contexts/event-context'; import addBasics from '../graphql/mutations/event/add-basics'; @@ -93,9 +93,7 @@ export default function Event() { - setOngoingTabNumber(newTabNumber) - } + onChange={(event, newTabNumber) => setOngoingTabNumber(newTabNumber)} style={{ margin: 4 }} value={ongoingTabNumber} variant="fullWidth" @@ -142,13 +140,13 @@ export default function Event() { Other drafters are still making their picks... - Yell at them to hurry up! Also tell them to turn notifications - on so they will be alerted when they have a selection to make. + Yell at them to hurry up! Also tell them to turn notifications on so they will be + alerted when they have a selection to make. )} - {ongoingTabNumber === 1 && ( + {/* ongoingTabNumber === 1 && ( - )} + ) */} {ongoingTabNumber === 2 && ( - setFinishedTabNumber(newTabNumber) - } + onChange={(event, newTabNumber) => setFinishedTabNumber(newTabNumber)} style={{ margin: 4 }} value={finishedTabNumber} variant="fullWidth" @@ -205,13 +201,13 @@ export default function Event() { }); }} /> - + /> */} )} diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 4f0080e..03a3631 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -11,6 +11,7 @@ import MUIListItem from '@mui/material/ListItem'; import MUIListItemIcon from '@mui/material/ListItemIcon'; import MUIListItemText from '@mui/material/ListItemText'; import MUITypography from '@mui/material/Typography'; +import searchPrintings from '../graphql/queries/card/search-printings'; export default function Home() { const navigate = useNavigate(); diff --git a/src/pages/PasswordReset.jsx b/src/pages/PasswordReset.jsx index 884b583..adf5763 100644 --- a/src/pages/PasswordReset.jsx +++ b/src/pages/PasswordReset.jsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { useContext, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import Cookies from 'js-cookie'; import MUIButton from '@mui/material/Button'; import MUICard from '@mui/material/Card'; import MUICardActions from '@mui/material/CardActions'; @@ -7,35 +8,62 @@ import MUICardContent from '@mui/material/CardContent'; import MUICardHeader from '@mui/material/CardHeader'; import MUITextField from '@mui/material/TextField'; +import submitPasswordReset from '../graphql/mutations/account/submit-password-reset'; +import tokenQuery from '../constants/token-query'; import LoadingSpinner from '../components/miscellaneous/LoadingSpinner'; import { AuthenticationContext } from '../contexts/Authentication'; import { ErrorContext } from '../contexts/Error'; export default function PasswordReset() { - const { loading, submitPasswordReset } = React.useContext( - AuthenticationContext - ); - const { setErrorMessages } = React.useContext(ErrorContext); - const confirmPasswordInput = React.useRef(); - const emailInput = React.useRef(); + const { abortControllerRef, loading, setLoading, setUserInfo } = + useContext(AuthenticationContext); + const { setErrorMessages } = useContext(ErrorContext); const navigate = useNavigate(); - const passwordInput = React.useRef(); const { resetToken } = useParams(); + const [emailInputState, setEmailInputState] = useState(''); + const [passwordInputState, setPasswordInputState] = useState(''); + const [confirmPasswordInputState, setConfirmPasswordInputState] = useState(''); async function submitForm(event) { event.preventDefault(); - if (passwordInput.current.value !== confirmPasswordInput.current.value) { + if (passwordInputState !== confirmPasswordInputState) { setErrorMessages((prevState) => [ ...prevState, 'The entered passwords do not match. Please try again.' ]); } else { - submitPasswordReset( - emailInput.current.value, - passwordInput.current.value, - resetToken - ); - navigate('/'); + try { + setLoading(true); + const { + data: { + submitPasswordReset: { + admin, + avatar, + measurement_system, + radius, + token, + userID, + userName + } + } + } = await submitPasswordReset({ + queryString: tokenQuery, + signal: abortControllerRef.current.signal, + variables: { + email: emailInputState, + password: passwordInputState, + reset_token: resetToken + } + }); + setUserInfo({ admin, avatar, measurement_system, radius, userID, userName }); + Cookies.set('authentication_token', token); + setTimeout(() => { + navigate('/'); + }, 0); + } catch (error) { + setLoading(false); + setErrorMessages((prevState) => [...prevState, error.message]); + } } } @@ -50,24 +78,27 @@ export default function PasswordReset() { )} diff --git a/src/theme.js b/src/theme.js index d5c3668..1b3e0a6 100644 --- a/src/theme.js +++ b/src/theme.js @@ -50,6 +50,14 @@ let theme = createTheme({ } } }, + MuiAvatarGroup: { + styleOverrides: { + avatar: { + height: 50, + width: 50 + } + } + }, MuiButton: { defaultProps: { size: 'small', @@ -91,19 +99,18 @@ let theme = createTheme({ display: 'flex', flex: '0 1 auto', flexDirection: 'column', - marginLeft: 8, - marginRight: 0, - marginTop: 0 + margin: 0 }, avatar: { alignItems: 'flex-start', alignSelf: 'stretch', - marginRight: 8 + margin: 0 }, content: { wordBreak: 'break-word' }, root: { + columnGap: 8, padding: 8 } } @@ -144,6 +151,14 @@ let theme = createTheme({ } } }, + MuiDrawer: { + styleOverrides: { + paper: { + background: `linear-gradient(to bottom, ${primaryColor.A700}, calc(2/3 * 100%), ${secondaryColor.A400})`, + margin: 0 + } + } + }, // MuiFormControl: { // defaultProps: { // margin: 'normal'