Compare commits
	
		
			613 Commits
		
	
	
		
			feature/em
			...
			fix/tx
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0fce9af77c | ||
| 
						 | 
					55c68c580a | ||
| 
						 | 
					832a7997d1 | ||
| 
						 | 
					4528e5a16e | ||
| 
						 | 
					38f064c6d8 | ||
| 
						 | 
					fbf4565dbc | ||
| 
						 | 
					9001c64fed | ||
| 
						 | 
					03b768db4e | ||
| 
						 | 
					825af0db89 | ||
| 
						 | 
					31043f33ab | ||
| 
						 | 
					39699a1cb9 | ||
| 
						 | 
					b50b300307 | ||
| 
						 | 
					82c06cbb12 | ||
| 
						 | 
					423ee18e6a | ||
| 
						 | 
					3bb26d0c9b | ||
| 
						 | 
					43c83d0de6 | ||
| 
						 | 
					6bb407cb0f | ||
| 
						 | 
					d7b29ba809 | ||
| 
						 | 
					ca81d8ad41 | ||
| 
						 | 
					a7e59d7b73 | ||
| 
						 | 
					8f6b28cef5 | ||
| 
						 | 
					c1815f272b | ||
| 
						 | 
					76871a8041 | ||
| 
						 | 
					d2033c8035 | ||
| 
						 | 
					48daf1c5c8 | ||
| 
						 | 
					a3365e4beb | ||
| 
						 | 
					45d813cdad | ||
| 
						 | 
					911416aa2f | ||
| 
						 | 
					2d836af9ed | ||
| 
						 | 
					4f0fc838be | ||
| 
						 | 
					fa93912c38 | ||
| 
						 | 
					f5cb76c302 | ||
| 
						 | 
					df1d65dcab | ||
| 
						 | 
					1513f78991 | ||
| 
						 | 
					3a064f307b | ||
| 
						 | 
					6fca05f310 | ||
| 
						 | 
					31e67d382f | ||
| 
						 | 
					27475301e4 | ||
| 
						 | 
					934283976a | ||
| 
						 | 
					2d9ca2674e | ||
| 
						 | 
					221c727af6 | ||
| 
						 | 
					3b0a8c44c9 | ||
| 
						 | 
					0d9e9e7b45 | ||
| 
						 | 
					094f739b80 | ||
| 
						 | 
					a2077f9592 | ||
| 
						 | 
					59d9c5356c | ||
| 
						 | 
					2a10d525fb | ||
| 
						 | 
					2c2bf59bcd | ||
| 
						 | 
					79bd5da3d2 | ||
| 
						 | 
					1fab69ef9b | ||
| 
						 | 
					6418094b0f | ||
| 
						 | 
					2d82966b3b | ||
| 
						 | 
					dbbbfdb2f0 | ||
| 
						 | 
					9923dd9390 | ||
| 
						 | 
					052be354d1 | ||
| 
						 | 
					737523fe8d | ||
| 
						 | 
					034fc3423b | ||
| 
						 | 
					b23b0066bb | ||
| 
						 | 
					380b6b1afd | ||
| 
						 | 
					c7d77b26b5 | ||
| 
						 | 
					aa62251780 | ||
| 
						 | 
					4b2b4b25c0 | ||
| 
						 | 
					6dd5712573 | ||
| 
						 | 
					6611a94652 | ||
| 
						 | 
					93c5ef231e | ||
| 
						 | 
					53c2104b94 | ||
| 
						 | 
					c336ff8334 | ||
| 
						 | 
					2086291d4d | ||
| 
						 | 
					5505f0ac87 | ||
| 
						 | 
					2cf92b908d | ||
| 
						 | 
					70de876f75 | ||
| 
						 | 
					58cde29fff | ||
| 
						 | 
					a06fb06610 | ||
| 
						 | 
					1f5a9731bb | ||
| 
						 | 
					ef4f95ca3e | ||
| 
						 | 
					fb9814ec76 | ||
| 
						 | 
					7f6b989f15 | ||
| 
						 | 
					d459b2ee92 | ||
| 
						 | 
					6ee1a09aaa | ||
| 
						 | 
					dd2228fb35 | ||
| 
						 | 
					ca52a5e064 | ||
| 
						 | 
					df0f8abe62 | ||
| 
						 | 
					a6c4db1951 | ||
| 
						 | 
					1c91003164 | ||
| 
						 | 
					66be0efbbd | ||
| 
						 | 
					9ab64ec062 | ||
| 
						 | 
					e77a5e234f | ||
| 
						 | 
					d2f618512a | ||
| 
						 | 
					f5063de2c9 | ||
| 
						 | 
					1ee8dcb536 | ||
| 
						 | 
					7f6f9c11db | ||
| 
						 | 
					b2b7059774 | ||
| 
						 | 
					41ba096ef9 | ||
| 
						 | 
					8b72086c04 | ||
| 
						 | 
					895b34cc68 | ||
| 
						 | 
					b9da659f83 | ||
| 
						 | 
					3897f2d823 | ||
| 
						 | 
					6a3ff3e1d7 | ||
| 
						 | 
					bf792f1495 | ||
| 
						 | 
					df3210a663 | ||
| 
						 | 
					bad7730c32 | ||
| 
						 | 
					adb6a78549 | ||
| 
						 | 
					8cc27f20c3 | ||
| 
						 | 
					8e49a3f5f1 | ||
| 
						 | 
					3179757469 | ||
| 
						 | 
					554cfb3db9 | ||
| 
						 | 
					637a066f69 | ||
| 
						 | 
					c9a852e9be | ||
| 
						 | 
					307a5407eb | ||
| 
						 | 
					faa28845c8 | ||
| 
						 | 
					168d11d48e | ||
| 
						 | 
					60f2bb558c | ||
| 
						 | 
					fdf33b9f45 | ||
| 
						 | 
					d05180d148 | ||
| 
						 | 
					bfaa6be17d | ||
| 
						 | 
					9e368dec84 | ||
| 
						 | 
					25eec6980f | ||
| 
						 | 
					8e2f20c5ac | ||
| 
						 | 
					a3d094e873 | ||
| 
						 | 
					ef70bfb13a | ||
| 
						 | 
					c26c7c13d1 | ||
| 
						 | 
					fc461ddd0d | ||
| 
						 | 
					c7001f6089 | ||
| 
						 | 
					243cbfec08 | ||
| 
						 | 
					1295e7fa41 | ||
| 
						 | 
					793623d216 | ||
| 
						 | 
					0cde0eb240 | ||
| 
						 | 
					e2acb48e03 | ||
| 
						 | 
					a4373bb970 | ||
| 
						 | 
					cfb791092a | ||
| 
						 | 
					3fcbac5ed9 | ||
| 
						 | 
					c40b272ce8 | ||
| 
						 | 
					860ff66a8a | ||
| 
						 | 
					f4f700bea1 | ||
| 
						 | 
					789bc00ac3 | ||
| 
						 | 
					6a0aabdeda | ||
| 
						 | 
					175b6266e8 | ||
| 
						 | 
					621482e2ee | ||
| 
						 | 
					e55f48bc83 | ||
| 
						 | 
					3e9e26a46a | ||
| 
						 | 
					f0e730bb9b | ||
| 
						 | 
					6ce4828fc6 | ||
| 
						 | 
					bb0a246ae5 | ||
| 
						 | 
					3af2bad536 | ||
| 
						 | 
					4f1b877db0 | ||
| 
						 | 
					0289d64f5e | ||
| 
						 | 
					868a0bcf78 | ||
| 
						 | 
					aab2476a05 | ||
| 
						 | 
					cb25986d72 | ||
| 
						 | 
					309ad57173 | ||
| 
						 | 
					53afb1d3d1 | ||
| 
						 | 
					31ff7c0e28 | ||
| 
						 | 
					dfa35df465 | ||
| 
						 | 
					f163b052e1 | ||
| 
						 | 
					25c5b9c015 | ||
| 
						 | 
					407e3946ce | ||
| 
						 | 
					dc5b0d71eb | ||
| 
						 | 
					3fd6c3f50e | ||
| 
						 | 
					ec8bfc5eee | ||
| 
						 | 
					b4a0bcb90d | ||
| 
						 | 
					2c729e2aa4 | ||
| 
						 | 
					1cb2542170 | ||
| 
						 | 
					00b309df34 | ||
| 
						 | 
					a6fc730de6 | ||
| 
						 | 
					2245c5a221 | ||
| 
						 | 
					60c33661ad | ||
| 
						 | 
					ea21c85038 | ||
| 
						 | 
					5478f43609 | ||
| 
						 | 
					a9b64abb85 | ||
| 
						 | 
					c6ced424d8 | ||
| 
						 | 
					3a1159cffc | ||
| 
						 | 
					3136de1bd1 | ||
| 
						 | 
					67ffd3f1b4 | ||
| 
						 | 
					8508cb69c4 | ||
| 
						 | 
					89217d2633 | ||
| 
						 | 
					ba1b64391c | ||
| 
						 | 
					098d919a77 | ||
| 
						 | 
					b2af37ab4b | ||
| 
						 | 
					dcb7e94e86 | ||
| 
						 | 
					67848b3d8d | ||
| 
						 | 
					31a86263a1 | ||
| 
						 | 
					4d0025afc1 | ||
| 
						 | 
					f85bd2398d | ||
| 
						 | 
					a2a6596cc5 | ||
| 
						 | 
					37208ce97e | ||
| 
						 | 
					bf4042926d | ||
| 
						 | 
					3ccc1c16ac | ||
| 
						 | 
					135f0c91a1 | ||
| 
						 | 
					8f5786e242 | ||
| 
						 | 
					810eb4ca27 | ||
| 
						 | 
					e6574f9f12 | ||
| 
						 | 
					1a6726fabf | ||
| 
						 | 
					89f8671217 | ||
| 
						 | 
					fb5259221b | ||
| 
						 | 
					fd17f59616 | ||
| 
						 | 
					91bbc7ea61 | ||
| 
						 | 
					783d832c6d | ||
| 
						 | 
					698ca376e7 | ||
| 
						 | 
					bfd9e21ab8 | ||
| 
						 | 
					e46411f245 | ||
| 
						 | 
					08447c6b29 | ||
| 
						 | 
					9216cc6bf7 | ||
| 
						 | 
					5108b08e39 | ||
| 
						 | 
					6c46a4f809 | ||
| 
						 | 
					0ea88f0d32 | ||
| 
						 | 
					4c2e1f36f3 | ||
| 
						 | 
					fa5315fc0e | ||
| 
						 | 
					eda8b1550c | ||
| 
						 | 
					742b11374f | ||
| 
						 | 
					d16e83dcfa | ||
| 
						 | 
					155aa57784 | ||
| 
						 | 
					b88b6da7d9 | ||
| 
						 | 
					fa13f7e282 | ||
| 
						 | 
					f1a43ef758 | ||
| 
						 | 
					4217813fd7 | ||
| 
						 | 
					c588f7b1f3 | ||
| 
						 | 
					985e8ee820 | ||
| 
						 | 
					8832e76a0a | ||
| 
						 | 
					9777f1dbd1 | ||
| 
						 | 
					213d468aab | ||
| 
						 | 
					46becb0e7b | ||
| 
						 | 
					fad6bd100a | ||
| 
						 | 
					5a11f83fea | ||
| 
						 | 
					525338abf7 | ||
| 
						 | 
					ea977816a4 | ||
| 
						 | 
					0ee599a2b6 | ||
| 
						 | 
					02c59f8d79 | ||
| 
						 | 
					3d5b77e60a | ||
| 
						 | 
					92a167d47a | ||
| 
						 | 
					d41e263942 | ||
| 
						 | 
					bd1226fe90 | ||
| 
						 | 
					57403e42dd | ||
| 
						 | 
					2b42a96c4a | ||
| 
						 | 
					80d6bb691d | ||
| 
						 | 
					c7e4cd7c92 | ||
| 
						 | 
					4a22861860 | ||
| 
						 | 
					b09d029931 | ||
| 
						 | 
					b2dc49754f | ||
| 
						 | 
					6f636645f7 | ||
| 
						 | 
					377c963c7a | ||
| 
						 | 
					ae038f17ff | ||
| 
						 | 
					0d8f2c31e7 | ||
| 
						 | 
					da9986eb66 | ||
| 
						 | 
					a21350770e | ||
| 
						 | 
					49dfd43220 | ||
| 
						 | 
					4472957f5c | ||
| 
						 | 
					ca46707bb5 | ||
| 
						 | 
					704ebe4b92 | ||
| 
						 | 
					9a6ef2c393 | ||
| 
						 | 
					56203ce9c6 | ||
| 
						 | 
					933bdb5968 | ||
| 
						 | 
					864711697b | ||
| 
						 | 
					e5eaf09721 | ||
| 
						 | 
					d0dde56c67 | ||
| 
						 | 
					45c6927e72 | ||
| 
						 | 
					6014b6e79f | ||
| 
						 | 
					04a99227df | ||
| 
						 | 
					0965a1e898 | ||
| 
						 | 
					32445dbebf | ||
| 
						 | 
					1a1d4901aa | ||
| 
						 | 
					8b646c56dc | ||
| 
						 | 
					ac38bbc72c | ||
| 
						 | 
					bf1182351a | ||
| 
						 | 
					55e48a943b | ||
| 
						 | 
					faf417be69 | ||
| 
						 | 
					c2eb57211f | ||
| 
						 | 
					0e97df3c8e | ||
| 
						 | 
					5dd0dfdc18 | ||
| 
						 | 
					ef48bac8f6 | ||
| 
						 | 
					3a3d984098 | ||
| 
						 | 
					2300c201f8 | ||
| 
						 | 
					329dc4a355 | ||
| 
						 | 
					cd6a5b23d4 | ||
| 
						 | 
					4dd7cbe2ca | ||
| 
						 | 
					260de7c838 | ||
| 
						 | 
					e0ed31f220 | ||
| 
						 | 
					eba183497f | ||
| 
						 | 
					4378afa9a1 | ||
| 
						 | 
					491e10920b | ||
| 
						 | 
					65bb209713 | ||
| 
						 | 
					c07e70acc9 | ||
| 
						 | 
					8fd7f8ecad | ||
| 
						 | 
					2bb3c646db | ||
| 
						 | 
					87f10a11b0 | ||
| 
						 | 
					949fb45ae2 | ||
| 
						 | 
					ea52f014dd | ||
| 
						 | 
					77eab8d88d | ||
| 
						 | 
					4ca8f5f236 | ||
| 
						 | 
					53f2a71b08 | ||
| 
						 | 
					866f6257f1 | ||
| 
						 | 
					814b074cc0 | ||
| 
						 | 
					386619619b | ||
| 
						 | 
					d8bf10d0b8 | ||
| 
						 | 
					d18c893025 | ||
| 
						 | 
					822f7a30f5 | ||
| 
						 | 
					1d66137c23 | ||
| 
						 | 
					4c42e75686 | ||
| 
						 | 
					501b7fefec | ||
| 
						 | 
					aa7e1517a2 | ||
| 
						 | 
					e33093f160 | ||
| 
						 | 
					923b689c98 | ||
| 
						 | 
					246e7f137f | ||
| 
						 | 
					5defd12a11 | ||
| 
						 | 
					abb7c2bb28 | ||
| 
						 | 
					12013907f8 | ||
| 
						 | 
					ec75fff74b | ||
| 
						 | 
					5e997044ed | ||
| 
						 | 
					e88720327e | ||
| 
						 | 
					bf568c3f46 | ||
| 
						 | 
					7c1068449f | ||
| 
						 | 
					b66d2a09a0 | ||
| 
						 | 
					1d3bd128f8 | ||
| 
						 | 
					ab1f45febd | ||
| 
						 | 
					54265e024c | ||
| 
						 | 
					20cb66ba18 | ||
| 
						 | 
					56a9806b70 | ||
| 
						 | 
					b3f2d0fb6d | ||
| 
						 | 
					b7d62dda83 | ||
| 
						 | 
					c690334f92 | ||
| 
						 | 
					587f09ec00 | ||
| 
						 | 
					9296ea1acc | ||
| 
						 | 
					582fb17c94 | ||
| 
						 | 
					aff0142870 | ||
| 
						 | 
					df51d87cb2 | ||
| 
						 | 
					6a46f5f173 | ||
| 
						 | 
					9e25cefef9 | ||
| 
						 | 
					95966fa514 | ||
| 
						 | 
					f49d69e75d | ||
| 
						 | 
					da4b2e68ab | ||
| 
						 | 
					5557b1bcba | ||
| 
						 | 
					f4b5f98a44 | ||
| 
						 | 
					b1d39740de | ||
| 
						 | 
					dfe5589074 | ||
| 
						 | 
					cdc50da840 | ||
| 
						 | 
					4893b41936 | ||
| 
						 | 
					16cbdafb27 | ||
| 
						 | 
					5559fb7be3 | ||
| 
						 | 
					3c4305127b | ||
| 
						 | 
					2a76fa0c35 | ||
| 
						 | 
					bf21fe36c3 | ||
| 
						 | 
					a33a3eb6e2 | ||
| 
						 | 
					919c4e173c | ||
| 
						 | 
					650324f434 | ||
| 
						 | 
					74db96e8a5 | ||
| 
						 | 
					c99c821081 | ||
| 
						 | 
					e53a533026 | ||
| 
						 | 
					5f118e00cb | ||
| 
						 | 
					46e6927c68 | ||
| 
						 | 
					de95a82c5a | ||
| 
						 | 
					3b66d64c14 | ||
| 
						 | 
					6e90a4c3d7 | ||
| 
						 | 
					2287e7babb | ||
| 
						 | 
					c219f7ea00 | ||
| 
						 | 
					e795ce4472 | ||
| 
						 | 
					04e2274dbf | ||
| 
						 | 
					6e39b90c1e | ||
| 
						 | 
					f186a807c1 | ||
| 
						 | 
					5ad9ed1688 | ||
| 
						 | 
					842b8a5226 | ||
| 
						 | 
					234832138f | ||
| 
						 | 
					28d94a1475 | ||
| 
						 | 
					594aee6cd2 | ||
| 
						 | 
					d75910972f | ||
| 
						 | 
					589c604a12 | ||
| 
						 | 
					8394a11705 | ||
| 
						 | 
					4ad329882c | ||
| 
						 | 
					ee86b91e82 | ||
| 
						 | 
					d2addf782e | ||
| 
						 | 
					51f7bd509b | ||
| 
						 | 
					e064251ff9 | ||
| 
						 | 
					5aeed7c246 | ||
| 
						 | 
					8d03edc299 | ||
| 
						 | 
					95022ef121 | ||
| 
						 | 
					4519906b78 | ||
| 
						 | 
					88a47c49a4 | ||
| 
						 | 
					1ab03f9bed | ||
| 
						 | 
					84ff667135 | ||
| 
						 | 
					0d10e782f3 | ||
| 
						 | 
					84e6763495 | ||
| 
						 | 
					7ffcfd694d | ||
| 
						 | 
					77e4917d38 | ||
| 
						 | 
					e4238a40cc | ||
| 
						 | 
					42c0b20512 | ||
| 
						 | 
					43154ff6d8 | ||
| 
						 | 
					8197b510f9 | ||
| 
						 | 
					fc7652f48e | ||
| 
						 | 
					bd32555617 | ||
| 
						 | 
					fc6f420e1e | ||
| 
						 | 
					d3c36765de | ||
| 
						 | 
					2628a12673 | ||
| 
						 | 
					f6c1869b5d | ||
| 
						 | 
					62c8b4f217 | ||
| 
						 | 
					8798e5a233 | ||
| 
						 | 
					5f7d42843c | ||
| 
						 | 
					302b36dde8 | ||
| 
						 | 
					3e7c7b1969 | ||
| 
						 | 
					936bbc503a | ||
| 
						 | 
					81890c8833 | ||
| 
						 | 
					50fa20c39a | ||
| 
						 | 
					11f2cffc87 | ||
| 
						 | 
					bbd1d162f0 | ||
| 
						 | 
					b301a860bf | ||
| 
						 | 
					ff697b96ea | ||
| 
						 | 
					48e9898e31 | ||
| 
						 | 
					2e25242ebe | ||
| 
						 | 
					e32e07f7fd | ||
| 
						 | 
					0d2a17008e | ||
| 
						 | 
					a87b3de6c4 | ||
| 
						 | 
					23068ff477 | ||
| 
						 | 
					a12a5dfbac | ||
| 
						 | 
					5a598cb091 | ||
| 
						 | 
					be39054a2f | ||
| 
						 | 
					0add65dd1c | ||
| 
						 | 
					82170ad4f8 | ||
| 
						 | 
					af49426eb0 | ||
| 
						 | 
					48a86e3386 | ||
| 
						 | 
					c82c35b5a1 | ||
| 
						 | 
					f849be1f80 | ||
| 
						 | 
					694d07fa0e | ||
| 
						 | 
					b9aa3e2adc | ||
| 
						 | 
					5b573b2379 | ||
| 
						 | 
					23538b1502 | ||
| 
						 | 
					723602ebdc | ||
| 
						 | 
					f8fdeaf9ce | ||
| 
						 | 
					e75b971718 | ||
| 
						 | 
					11a35a5932 | ||
| 
						 | 
					611f875761 | ||
| 
						 | 
					a7df50c194 | ||
| 
						 | 
					0c6c60ed29 | ||
| 
						 | 
					e82662647f | ||
| 
						 | 
					5490e7205a | ||
| 
						 | 
					d8e218392a | ||
| 
						 | 
					723722df58 | ||
| 
						 | 
					2ff85ede06 | ||
| 
						 | 
					052a1e5b60 | ||
| 
						 | 
					7f8f47cb14 | ||
| 
						 | 
					ddb043c104 | ||
| 
						 | 
					d2ad6537d7 | ||
| 
						 | 
					8f004ee4da | ||
| 
						 | 
					b90bf67c20 | ||
| 
						 | 
					746112e637 | ||
| 
						 | 
					13bfd42093 | ||
| 
						 | 
					c1f7d7d51c | ||
| 
						 | 
					38a097a8f9 | ||
| 
						 | 
					db0ffe999e | ||
| 
						 | 
					6d88c4e546 | ||
| 
						 | 
					09f58f18ae | ||
| 
						 | 
					e11ddaffb0 | ||
| 
						 | 
					2e88f568b8 | ||
| 
						 | 
					237d504f17 | ||
| 
						 | 
					197fc09e1d | ||
| 
						 | 
					2c74a93aee | ||
| 
						 | 
					5209644780 | ||
| 
						 | 
					ed37427da8 | ||
| 
						 | 
					daee9de96c | ||
| 
						 | 
					e4b10d12c2 | ||
| 
						 | 
					64eabb4502 | ||
| 
						 | 
					12a24d3d86 | ||
| 
						 | 
					ce91182c7b | ||
| 
						 | 
					2e3a0e557e | ||
| 
						 | 
					395e02343b | ||
| 
						 | 
					3682dd4946 | ||
| 
						 | 
					3070ed706e | ||
| 
						 | 
					4b73687779 | ||
| 
						 | 
					6b9a9ef978 | ||
| 
						 | 
					bc5bb5be39 | ||
| 
						 | 
					0fe83811b9 | ||
| 
						 | 
					c6359aa853 | ||
| 
						 | 
					c9c818c8f3 | ||
| 
						 | 
					c521246393 | ||
| 
						 | 
					8936b34361 | ||
| 
						 | 
					5993d2762f | ||
| 
						 | 
					0a44b5b5d1 | ||
| 
						 | 
					810d3b2524 | ||
| 
						 | 
					a3393ded1e | ||
| 
						 | 
					17ede265b1 | ||
| 
						 | 
					629070edad | ||
| 
						 | 
					cc83924c27 | ||
| 
						 | 
					e3e964f72a | ||
| 
						 | 
					0def1d30a6 | ||
| 
						 | 
					bdb2c0cf8f | ||
| 
						 | 
					3e8dbc9793 | ||
| 
						 | 
					d735cd3833 | ||
| 
						 | 
					eddf228283 | ||
| 
						 | 
					f9d617efdc | ||
| 
						 | 
					43796021da | ||
| 
						 | 
					241c21782d | ||
| 
						 | 
					1a3f5d144c | ||
| 
						 | 
					66fb68d52e | ||
| 
						 | 
					aaeb32a576 | ||
| 
						 | 
					78b5dcceb6 | ||
| 
						 | 
					ce81c11c29 | ||
| 
						 | 
					3a98b95e3d | ||
| 
						 | 
					8bc36655e2 | ||
| 
						 | 
					b6ab536a60 | ||
| 
						 | 
					37a3d2b207 | ||
| 
						 | 
					cd0c5f8a0d | ||
| 
						 | 
					8dde89fa9a | ||
| 
						 | 
					ca3d60cfb8 | ||
| 
						 | 
					1a4d53cfbc | ||
| 
						 | 
					94e126782b | ||
| 
						 | 
					cc03c64f0a | ||
| 
						 | 
					3647aa6274 | ||
| 
						 | 
					a2a58f0ba9 | ||
| 
						 | 
					c544a03be4 | ||
| 
						 | 
					9a09da88ec | ||
| 
						 | 
					5850551906 | ||
| 
						 | 
					e35e520d24 | ||
| 
						 | 
					8077fc5865 | ||
| 
						 | 
					bff01b4a9f | ||
| 
						 | 
					de5380d6f3 | ||
| 
						 | 
					eda2a9596a | ||
| 
						 | 
					195d33b1db | ||
| 
						 | 
					4f042ef3b7 | ||
| 
						 | 
					17c67822a9 | ||
| 
						 | 
					e6f613ae0b | ||
| 
						 | 
					9b822cfda4 | ||
| 
						 | 
					b5b918d877 | ||
| 
						 | 
					739918647d | ||
| 
						 | 
					1f334d6253 | ||
| 
						 | 
					0f15a85c45 | ||
| 
						 | 
					0c4330e329 | ||
| 
						 | 
					a9676288ea | ||
| 
						 | 
					7354474c70 | ||
| 
						 | 
					ce5b307a8b | ||
| 
						 | 
					b28bcfdd0a | ||
| 
						 | 
					7f06876e3e | ||
| 
						 | 
					fd479d8671 | ||
| 
						 | 
					938b567256 | ||
| 
						 | 
					779f5aab0a | ||
| 
						 | 
					02194d8a98 | ||
| 
						 | 
					5677fe34dc | ||
| 
						 | 
					895da89325 | ||
| 
						 | 
					b138cc8d5b | ||
| 
						 | 
					027b2c8ed4 | ||
| 
						 | 
					d85cc71817 | ||
| 
						 | 
					bac3522078 | ||
| 
						 | 
					b2c6aa7871 | ||
| 
						 | 
					81e2a3673d | ||
| 
						 | 
					b4ca360661 | ||
| 
						 | 
					ad947be0bc | ||
| 
						 | 
					f739d4da34 | ||
| 
						 | 
					fdb1eb01a4 | ||
| 
						 | 
					920d359966 | ||
| 
						 | 
					9e1dbc8765 | ||
| 
						 | 
					10ea77fd8d | ||
| 
						 | 
					50de7ebf15 | ||
| 
						 | 
					7db07e3f92 | ||
| 
						 | 
					6ad7c67672 | ||
| 
						 | 
					10f279a6b4 | ||
| 
						 | 
					792c093cfd | ||
| 
						 | 
					a11a641608 | ||
| 
						 | 
					c3bf31d993 | ||
| 
						 | 
					67d1b72331 | ||
| 
						 | 
					35bc89cf99 | ||
| 
						 | 
					380e196db2 | ||
| 
						 | 
					d67613c0cf | ||
| 
						 | 
					4d4b96bede | ||
| 
						 | 
					59637e32fe | ||
| 
						 | 
					82d0c8c5ff | ||
| 
						 | 
					b41ee2198b | ||
| 
						 | 
					09c5aff1da | ||
| 
						 | 
					d806a46f13 | ||
| 
						 | 
					6bcbb5d6df | ||
| 
						 | 
					0d7a4aae10 | ||
| 
						 | 
					276dfff2ba | ||
| 
						 | 
					eddb870f85 | ||
| 
						 | 
					3707a215bb | ||
| 
						 | 
					fad5e13430 | ||
| 
						 | 
					df47158f29 | ||
| 
						 | 
					51e4fed345 | ||
| 
						 | 
					e471e8d7ef | ||
| 
						 | 
					adc268c3cd | ||
| 
						 | 
					69e08abbc9 | ||
| 
						 | 
					b8596ec7ce | ||
| 
						 | 
					4fc7098e78 | ||
| 
						 | 
					69c7865491 | ||
| 
						 | 
					8ac7e82221 | ||
| 
						 | 
					5eea51744e | ||
| 
						 | 
					dcf0598852 | ||
| 
						 | 
					a7d04a28e4 | ||
| 
						 | 
					a0303ecfa4 | ||
| 
						 | 
					5a79e07c2d | ||
| 
						 | 
					1107bb8196 | ||
| 
						 | 
					405aafed7e | ||
| 
						 | 
					03156474f3 | ||
| 
						 | 
					7982209732 | ||
| 
						 | 
					0f9963b972 | ||
| 
						 | 
					650243279a | ||
| 
						 | 
					6f183049d5 | ||
| 
						 | 
					48706effc1 | ||
| 
						 | 
					c9740b1e8a | ||
| 
						 | 
					166300b8d5 | ||
| 
						 | 
					99968855e0 | ||
| 
						 | 
					a62a9c3700 | ||
| 
						 | 
					4e971ce119 | ||
| 
						 | 
					72ba2072ec | ||
| 
						 | 
					bd8d3c39c2 | ||
| 
						 | 
					7142f5b5e2 | ||
| 
						 | 
					37516c602d | ||
| 
						 | 
					cfa7a3bd30 | ||
| 
						 | 
					266e4c3e6c | ||
| 
						 | 
					4cf6d376f0 | ||
| 
						 | 
					b2f49625db | ||
| 
						 | 
					0b9fd172ce | ||
| 
						 | 
					2582981d85 | ||
| 
						 | 
					a0eda59982 | ||
| 
						 | 
					9ae9db984d | ||
| 
						 | 
					8f2c78b08b | ||
| 
						 | 
					460412d3d7 | ||
| 
						 | 
					1a182858b4 | ||
| 
						 | 
					baac750e43 | ||
| 
						 | 
					74979decbe | 
							
								
								
									
										11
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						@@ -1,3 +1,12 @@
 | 
			
		||||
NEXTAUTH_URL=https://example.com
 | 
			
		||||
NEXTAUTH_SECRET="1234"
 | 
			
		||||
GITHUB_SECRET=""
 | 
			
		||||
GITHUB_ID=""
 | 
			
		||||
GITHUB_ID=""
 | 
			
		||||
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
 | 
			
		||||
NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000"
 | 
			
		||||
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
 | 
			
		||||
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v3.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v3-debugstream.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v3-explorer.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_NETWORK_ID="21338"
 | 
			
		||||
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
 | 
			
		||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -32,3 +32,8 @@ yarn-error.log*
 | 
			
		||||
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
.vscode
 | 
			
		||||
 | 
			
		||||
# yarn
 | 
			
		||||
.yarnrc.yml
 | 
			
		||||
.yarn/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
/.pnp
 | 
			
		||||
.pnp.js
 | 
			
		||||
 | 
			
		||||
# testing
 | 
			
		||||
/coverage
 | 
			
		||||
 | 
			
		||||
# next.js
 | 
			
		||||
/.next/
 | 
			
		||||
/out/
 | 
			
		||||
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.pem
 | 
			
		||||
 | 
			
		||||
# debug
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
 | 
			
		||||
# local env files
 | 
			
		||||
.env.local
 | 
			
		||||
.env.development.local
 | 
			
		||||
.env.test.local
 | 
			
		||||
.env.production.local
 | 
			
		||||
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
.vscode
 | 
			
		||||
 | 
			
		||||
*.md
 | 
			
		||||
utils/libwabt.js
 | 
			
		||||
							
								
								
									
										8
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "tabWidth": 2,
 | 
			
		||||
  "arrowParens": "avoid",
 | 
			
		||||
  "semi": false,
 | 
			
		||||
  "printWidth": 100,
 | 
			
		||||
  "singleQuote": true,
 | 
			
		||||
  "trailingComma": "none"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										86
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,6 +1,8 @@
 | 
			
		||||
# XRPL Hooks IDE
 | 
			
		||||
# XRPL Hooks Builder
 | 
			
		||||
 | 
			
		||||
This is the repository for XRPL Hooks IDE. This project is built with Next.JS
 | 
			
		||||
https://hooks-builder.xrpl.org/
 | 
			
		||||
 | 
			
		||||
This is the repository for XRPL Hooks Builder. This project is built with Next.JS
 | 
			
		||||
 | 
			
		||||
## General
 | 
			
		||||
 | 
			
		||||
@@ -8,7 +10,9 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
 | 
			
		||||
 | 
			
		||||
## Getting Started
 | 
			
		||||
 | 
			
		||||
First, run the development server:
 | 
			
		||||
First, copy the `.env.example` to `.env.local` file, someone from the team can provide you your enviroment variables.
 | 
			
		||||
 | 
			
		||||
Then, run the development server:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm run dev
 | 
			
		||||
@@ -24,6 +28,78 @@ You can start editing the page by modifying `pages/index.tsx`. The page auto-upd
 | 
			
		||||
 | 
			
		||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
 | 
			
		||||
 | 
			
		||||
## Github Login
 | 
			
		||||
 | 
			
		||||
If you want to use your Github app to provide login, here's the guide to do that.
 | 
			
		||||
 | 
			
		||||
- First go to https://github.com/settings/profile -> Developer Settings -> OAuth Apps
 | 
			
		||||
- Click "New OAuth App" button
 | 
			
		||||
- Give application some name eg. Xrpl Hooks Development
 | 
			
		||||
- Give some homepage url eg. localhost:3000
 | 
			
		||||
- Give some optional description (these values will show up on the popup when you login)
 | 
			
		||||
- Authorization callback URL should be http://localhost:3000/api/auth/callback (if you're creating the app for local development)
 | 
			
		||||
- Click register application
 | 
			
		||||
- Then a page should open up where you can get client id and client secret values. Copy paste those to .env.local to use them:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
GITHUB_SECRET="client-secret-here"
 | 
			
		||||
GITHUB_ID="client-id-here"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Login should now work through your own Github OAuth app.
 | 
			
		||||
 | 
			
		||||
## Styling and Theming
 | 
			
		||||
 | 
			
		||||
This project uses Stitches (https://stitches.dev) for theming and styling the components. You should be quite familiar with the API if you have used for example styled-components earlier. Stitches should provide better performance, near zero runtime.
 | 
			
		||||
 | 
			
		||||
For components we try to use Radix-UI (https://www.radix-ui.com/) as much as possible. It may not provide all the necessary components so you're free to use other components/libraries if those makes sense. For colors we're using Radix-UI Colors (https://radix-ui.com/colors).
 | 
			
		||||
 | 
			
		||||
Theme file can be found under `./stitches.config.ts` file. When you're creating new components remeber to import `styled` from that file and not `@stitches` directly. That way it will provide the correct theme for you automatically.
 | 
			
		||||
 | 
			
		||||
Example:
 | 
			
		||||
 | 
			
		||||
```tsx
 | 
			
		||||
// Use our stitches.config instead of @stitches/react
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const Box = styled("div", {
 | 
			
		||||
  boxSizing: "border-box",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Box;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Custom components can be found from `./components` folder.
 | 
			
		||||
 | 
			
		||||
## Monaco Editor
 | 
			
		||||
 | 
			
		||||
Project is relying on Monaco editor heavily. Instead of using Monaco editor directly we're using `@monaco-editor/react` which provides little helpers to use Monaco editor.
 | 
			
		||||
 | 
			
		||||
On the Develop page we're using following loader for Monaco editor:
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
loader.config({
 | 
			
		||||
  paths: {
 | 
			
		||||
    vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
By default `@monaco-editor/react` was using 0.29.? version of Monaco editor. @codingame/monaco-languageclient library (connects to clangd language server) was using API methods that were introduced in Monaco Editor 0.30 so that's why we're loading certain version of it.
 | 
			
		||||
 | 
			
		||||
Monaco Languageclient related stuff is found from `./utils/languageClient.ts`. Basically we're connecting the editor to clangd language server which lives on separate backend. That project can be found from https://github.com/eqlabs/xrpl-hooks-compiler/. If you need access to that project ask permissions from @vbar (Vaclav Barta) on GitHub.
 | 
			
		||||
 | 
			
		||||
### Language server hover messages
 | 
			
		||||
If you want to extend hover messages provided by language-server you can add extra docs to `xrpl-hooks-docs/md/` folder. Just make sure the filename is matching with the error code that comes from language server. So lets say you want to add extra documentation for `hooks-func-addr-taken` check create new file called `hooks-func-addr-taken.md` and then remember to import and export it on `docs.ts` file with same logic as the other files.
 | 
			
		||||
 | 
			
		||||
## Global state management
 | 
			
		||||
 | 
			
		||||
Global state management is handled with library called Valtio (https://github.com/pmndrs/valtio). Initial state can be found from `./state/index.ts` file. All the actions which updates the state is found under `./state/actions/` folder.
 | 
			
		||||
 | 
			
		||||
## Special notes
 | 
			
		||||
 | 
			
		||||
Since we are dealing with greenfield tech and one of the dependencies (ripple-binary-codec) doesn't yet support signing `SetHook` transactions we had to monkey patch the library with patch-package (https://www.npmjs.com/package/patch-package). We modified the definitions.json file of the ripple-binary-codec library and then ran `yarn patch-package ripple-binary-codec` which created `patches/ripple-binary-codec+1.2.0.patch` file to this project. This file contains the modifications to `ripple-binary-codec` library. package.json contains postinstall hook which runs patch-package, and it will add the patch on the file mentioned earlier. This happens automatically after running patch package.
 | 
			
		||||
 | 
			
		||||
## Learn More
 | 
			
		||||
 | 
			
		||||
To learn more about Next.js, take a look at the following resources:
 | 
			
		||||
@@ -33,8 +109,4 @@ To learn more about Next.js, take a look at the following resources:
 | 
			
		||||
 | 
			
		||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
 | 
			
		||||
 | 
			
		||||
## Deploy on Vercel
 | 
			
		||||
 | 
			
		||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
 | 
			
		||||
 | 
			
		||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										589
									
								
								components/Accounts.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,589 @@
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import { ArrowSquareOut, Copy, Trash, Wallet, X } from 'phosphor-react'
 | 
			
		||||
import React, { useEffect, useState, FC } from 'react'
 | 
			
		||||
import Dinero from 'dinero.js'
 | 
			
		||||
 | 
			
		||||
import Button from './Button'
 | 
			
		||||
import { addFaucetAccount, importAccount } from '../state/actions'
 | 
			
		||||
import state from '../state'
 | 
			
		||||
import Box from './Box'
 | 
			
		||||
import { Container, Heading, Stack, Text, Flex } from '.'
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogClose,
 | 
			
		||||
  DialogTrigger
 | 
			
		||||
} from './Dialog'
 | 
			
		||||
import { css } from '../stitches.config'
 | 
			
		||||
import { Input, Label } from './Input'
 | 
			
		||||
import truncate from '../utils/truncate'
 | 
			
		||||
 | 
			
		||||
const labelStyle = css({
 | 
			
		||||
  color: '$mauve10',
 | 
			
		||||
  textTransform: 'uppercase',
 | 
			
		||||
  fontSize: '10px',
 | 
			
		||||
  mb: '$0.5'
 | 
			
		||||
})
 | 
			
		||||
import transactionsData from '../content/transactions.json'
 | 
			
		||||
import { SetHookDialog } from './SetHookDialog'
 | 
			
		||||
import { addFunds } from '../state/actions/addFaucetAccount'
 | 
			
		||||
import { deleteHook } from '../state/actions/deployHook'
 | 
			
		||||
import { capitalize } from '../utils/helpers'
 | 
			
		||||
import { deleteAccount } from '../state/actions/deleteAccount'
 | 
			
		||||
import { xrplSend } from '../state/actions/xrpl-client'
 | 
			
		||||
 | 
			
		||||
export const AccountDialog = ({
 | 
			
		||||
  activeAccountAddress,
 | 
			
		||||
  setActiveAccountAddress
 | 
			
		||||
}: {
 | 
			
		||||
  activeAccountAddress: string | null
 | 
			
		||||
  setActiveAccountAddress: React.Dispatch<React.SetStateAction<string | null>>
 | 
			
		||||
}) => {
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  const [showSecret, setShowSecret] = useState(false)
 | 
			
		||||
  const activeAccount = snap.accounts.find(account => account.address === activeAccountAddress)
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog
 | 
			
		||||
      open={Boolean(activeAccountAddress)}
 | 
			
		||||
      onOpenChange={open => {
 | 
			
		||||
        setShowSecret(false)
 | 
			
		||||
        !open && setActiveAccountAddress(null)
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <DialogContent
 | 
			
		||||
        css={{
 | 
			
		||||
          backgroundColor: '$mauve1 !important',
 | 
			
		||||
          border: '1px solid $mauve2',
 | 
			
		||||
          '.dark &': {
 | 
			
		||||
            // backgroundColor: "$black !important",
 | 
			
		||||
          },
 | 
			
		||||
          p: '$3',
 | 
			
		||||
          '&:before': {
 | 
			
		||||
            content: ' ',
 | 
			
		||||
            position: 'absolute',
 | 
			
		||||
            top: 0,
 | 
			
		||||
            right: 0,
 | 
			
		||||
            bottom: 0,
 | 
			
		||||
            left: 0,
 | 
			
		||||
            opacity: 0.2,
 | 
			
		||||
            '.dark &': {
 | 
			
		||||
              opacity: 1
 | 
			
		||||
            },
 | 
			
		||||
            zIndex: 0,
 | 
			
		||||
            pointerEvents: 'none',
 | 
			
		||||
            backgroundImage: `url('/pattern-dark.svg'), url('/pattern-dark-2.svg')`,
 | 
			
		||||
            backgroundRepeat: 'no-repeat',
 | 
			
		||||
            backgroundPosition: 'bottom left, top right'
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <DialogTitle
 | 
			
		||||
          css={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            borderBottom: '1px solid $mauve6',
 | 
			
		||||
            pb: '$3',
 | 
			
		||||
            gap: '$3',
 | 
			
		||||
            fontSize: '$md'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Wallet size="15px" /> {activeAccount?.name}
 | 
			
		||||
          <DialogClose asChild>
 | 
			
		||||
            <Button
 | 
			
		||||
              size="xs"
 | 
			
		||||
              outline
 | 
			
		||||
              css={{ ml: 'auto', mr: '$9' }}
 | 
			
		||||
              tabIndex={-1}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                deleteAccount(activeAccount?.address)
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Delete Account <Trash size="15px" />
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
        </DialogTitle>
 | 
			
		||||
        <DialogDescription as="div" css={{ fontFamily: '$monospace' }}>
 | 
			
		||||
          <Stack css={{ display: 'flex', flexDirection: 'column', gap: '$3' }}>
 | 
			
		||||
            <Flex css={{ alignItems: 'center' }}>
 | 
			
		||||
              <Flex css={{ flexDirection: 'column' }}>
 | 
			
		||||
                <Text className={labelStyle()}>Account Address</Text>
 | 
			
		||||
                <Text
 | 
			
		||||
                  css={{
 | 
			
		||||
                    fontFamily: '$monospace',
 | 
			
		||||
                    a: { '&:hover': { textDecoration: 'underline' } }
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <a
 | 
			
		||||
                    href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                    rel="noopener noreferrer"
 | 
			
		||||
                  >
 | 
			
		||||
                    {activeAccount?.address}
 | 
			
		||||
                  </a>
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Flex>
 | 
			
		||||
              <Flex css={{ marginLeft: 'auto', color: '$mauve12' }}>
 | 
			
		||||
                <Button
 | 
			
		||||
                  size="sm"
 | 
			
		||||
                  ghost
 | 
			
		||||
                  css={{ mt: '$3' }}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    navigator.clipboard.writeText(activeAccount?.address || '')
 | 
			
		||||
                    toast.success('Copied address to clipboard')
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Copy size="15px" />
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Flex>
 | 
			
		||||
            </Flex>
 | 
			
		||||
            <Flex css={{ alignItems: 'center' }}>
 | 
			
		||||
              <Flex css={{ flexDirection: 'column' }}>
 | 
			
		||||
                <Text className={labelStyle()}>Secret</Text>
 | 
			
		||||
                <Text
 | 
			
		||||
                  as="div"
 | 
			
		||||
                  css={{
 | 
			
		||||
                    fontFamily: '$monospace',
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    alignItems: 'center'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {showSecret
 | 
			
		||||
                    ? activeAccount?.secret
 | 
			
		||||
                    : '•'.repeat(activeAccount?.secret.length || 16)}{' '}
 | 
			
		||||
                  <Button
 | 
			
		||||
                    css={{
 | 
			
		||||
                      fontFamily: '$monospace',
 | 
			
		||||
                      lineHeight: 2,
 | 
			
		||||
                      mt: '2px',
 | 
			
		||||
                      ml: '$3'
 | 
			
		||||
                    }}
 | 
			
		||||
                    ghost
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    onClick={() => setShowSecret(curr => !curr)}
 | 
			
		||||
                  >
 | 
			
		||||
                    {showSecret ? 'Hide' : 'Show'}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Flex>
 | 
			
		||||
              <Flex css={{ marginLeft: 'auto', color: '$mauve12' }}>
 | 
			
		||||
                <Button
 | 
			
		||||
                  size="sm"
 | 
			
		||||
                  ghost
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    navigator.clipboard.writeText(activeAccount?.secret || '')
 | 
			
		||||
                    toast.success('Copied secret to clipboard')
 | 
			
		||||
                  }}
 | 
			
		||||
                  css={{ mt: '$3' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Copy size="15px" />
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Flex>
 | 
			
		||||
            </Flex>
 | 
			
		||||
            <Flex css={{ alignItems: 'center' }}>
 | 
			
		||||
              <Flex css={{ flexDirection: 'column' }}>
 | 
			
		||||
                <Text className={labelStyle()}>Balances & Objects</Text>
 | 
			
		||||
                <Text
 | 
			
		||||
                  css={{
 | 
			
		||||
                    fontFamily: '$monospace',
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    alignItems: 'center'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {Dinero({
 | 
			
		||||
                    amount: Number(activeAccount?.xrp || '0'),
 | 
			
		||||
                    precision: 6
 | 
			
		||||
                  })
 | 
			
		||||
                    .toUnit()
 | 
			
		||||
                    .toLocaleString(undefined, {
 | 
			
		||||
                      style: 'currency',
 | 
			
		||||
                      currency: 'XRP',
 | 
			
		||||
                      currencyDisplay: 'name'
 | 
			
		||||
                    })}
 | 
			
		||||
                  <Button
 | 
			
		||||
                    css={{
 | 
			
		||||
                      fontFamily: '$monospace',
 | 
			
		||||
                      lineHeight: 2,
 | 
			
		||||
                      mt: '2px',
 | 
			
		||||
                      ml: '$3'
 | 
			
		||||
                    }}
 | 
			
		||||
                    ghost
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      addFunds(activeAccount?.address || '')
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Add Funds
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Flex>
 | 
			
		||||
              <Flex
 | 
			
		||||
                css={{
 | 
			
		||||
                  marginLeft: 'auto'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <a
 | 
			
		||||
                  href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
 | 
			
		||||
                  target="_blank"
 | 
			
		||||
                  rel="noreferrer noopener"
 | 
			
		||||
                >
 | 
			
		||||
                  <Button size="sm" ghost css={{ color: '$grass11 !important', mt: '$3' }}>
 | 
			
		||||
                    <ArrowSquareOut size="15px" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </a>
 | 
			
		||||
              </Flex>
 | 
			
		||||
            </Flex>
 | 
			
		||||
            <Flex css={{ alignItems: 'center' }}>
 | 
			
		||||
              <Flex css={{ flexDirection: 'column' }}>
 | 
			
		||||
                <Text className={labelStyle()}>Installed Hooks</Text>
 | 
			
		||||
                <Text
 | 
			
		||||
                  css={{
 | 
			
		||||
                    fontFamily: '$monospace',
 | 
			
		||||
                    a: { '&:hover': { textDecoration: 'underline' } }
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {activeAccount && activeAccount.hooks.length > 0
 | 
			
		||||
                    ? activeAccount.hooks.map(i => {
 | 
			
		||||
                        return (
 | 
			
		||||
                          <a
 | 
			
		||||
                            key={i}
 | 
			
		||||
                            href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${i}`}
 | 
			
		||||
                            target="_blank"
 | 
			
		||||
                            rel="noopener noreferrer"
 | 
			
		||||
                          >
 | 
			
		||||
                            {truncate(i, 12)}
 | 
			
		||||
                          </a>
 | 
			
		||||
                        )
 | 
			
		||||
                      })
 | 
			
		||||
                    : '–'}
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Flex>
 | 
			
		||||
              {activeAccount && activeAccount?.hooks?.length > 0 && (
 | 
			
		||||
                <Flex css={{ marginLeft: 'auto' }}>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    outline
 | 
			
		||||
                    disabled={activeAccount.isLoading}
 | 
			
		||||
                    css={{ mt: '$3', mr: '$1', ml: 'auto' }}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      deleteHook(activeAccount)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete Hook <Trash size="15px" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Flex>
 | 
			
		||||
              )}
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </DialogDescription>
 | 
			
		||||
        <DialogClose asChild>
 | 
			
		||||
          <Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
 | 
			
		||||
            <X size="20px" />
 | 
			
		||||
          </Box>
 | 
			
		||||
        </DialogClose>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface AccountProps {
 | 
			
		||||
  card?: boolean
 | 
			
		||||
  hideDeployBtn?: boolean
 | 
			
		||||
  showHookStats?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Accounts: FC<AccountProps> = props => {
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  const [activeAccountAddress, setActiveAccountAddress] = useState<string | null>(null)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchAccInfo = async () => {
 | 
			
		||||
      if (snap.clientStatus === 'online') {
 | 
			
		||||
        const requests = snap.accounts.map(acc =>
 | 
			
		||||
          xrplSend({
 | 
			
		||||
            id: `hooks-builder-req-info-${acc.address}`,
 | 
			
		||||
            command: 'account_info',
 | 
			
		||||
            account: acc.address
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
        const responses = await Promise.all(requests)
 | 
			
		||||
        responses.forEach((res: any) => {
 | 
			
		||||
          const address = res?.account_data?.Account as string
 | 
			
		||||
          const balance = res?.account_data?.Balance as string
 | 
			
		||||
          const sequence = res?.account_data?.Sequence as number
 | 
			
		||||
          const accountToUpdate = state.accounts.find(acc => acc.address === address)
 | 
			
		||||
          if (accountToUpdate) {
 | 
			
		||||
            accountToUpdate.xrp = balance
 | 
			
		||||
            accountToUpdate.sequence = sequence
 | 
			
		||||
            accountToUpdate.error = null
 | 
			
		||||
          } else {
 | 
			
		||||
            const oldAccount = state.accounts.find(acc => acc.address === res?.account)
 | 
			
		||||
            if (oldAccount) {
 | 
			
		||||
              oldAccount.xrp = '0'
 | 
			
		||||
              oldAccount.error = {
 | 
			
		||||
                code: res?.error,
 | 
			
		||||
                message: res?.error_message
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const objectRequests = snap.accounts.map(acc => {
 | 
			
		||||
          return xrplSend({
 | 
			
		||||
            id: `hooks-builder-req-objects-${acc.address}`,
 | 
			
		||||
            command: 'account_objects',
 | 
			
		||||
            account: acc.address
 | 
			
		||||
          })
 | 
			
		||||
        })
 | 
			
		||||
        const objectResponses = await Promise.all(objectRequests)
 | 
			
		||||
        objectResponses.forEach((res: any) => {
 | 
			
		||||
          const address = res?.account as string
 | 
			
		||||
          const accountToUpdate = state.accounts.find(acc => acc.address === address)
 | 
			
		||||
          if (accountToUpdate) {
 | 
			
		||||
            accountToUpdate.hooks =
 | 
			
		||||
              res.account_objects
 | 
			
		||||
                .find((ac: any) => ac?.LedgerEntryType === 'Hook')
 | 
			
		||||
                ?.Hooks?.map((oo: any) => oo.Hook.HookHash) || []
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let fetchAccountInfoInterval: NodeJS.Timer
 | 
			
		||||
    if (snap.clientStatus === 'online') {
 | 
			
		||||
      fetchAccInfo()
 | 
			
		||||
      fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 10000)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (snap.accounts.length > 0) {
 | 
			
		||||
        if (fetchAccountInfoInterval) {
 | 
			
		||||
          clearInterval(fetchAccountInfoInterval)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [snap.accounts.length, snap.clientStatus])
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      as="div"
 | 
			
		||||
      css={{
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        backgroundColor: props.card ? '$deep' : '$mauve1',
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        flex: '1',
 | 
			
		||||
        height: '100%',
 | 
			
		||||
        border: '1px solid $mauve6',
 | 
			
		||||
        borderRadius: props.card ? '$md' : undefined
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Container css={{ p: 0, flexShrink: 1, height: '100%' }}>
 | 
			
		||||
        <Flex
 | 
			
		||||
          css={{
 | 
			
		||||
            py: '$3',
 | 
			
		||||
            borderBottom: props.card ? '1px solid $mauve6' : undefined
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Heading
 | 
			
		||||
            as="h3"
 | 
			
		||||
            css={{
 | 
			
		||||
              fontWeight: 300,
 | 
			
		||||
              m: 0,
 | 
			
		||||
              fontSize: '11px',
 | 
			
		||||
              color: '$mauve12',
 | 
			
		||||
              px: '$3',
 | 
			
		||||
              textTransform: 'uppercase',
 | 
			
		||||
              alignItems: 'center',
 | 
			
		||||
              display: 'inline-flex',
 | 
			
		||||
              gap: '$3'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
 | 
			
		||||
          </Heading>
 | 
			
		||||
          <Flex css={{ ml: 'auto', gap: '$3', marginRight: '$3' }}>
 | 
			
		||||
            <ImportAccountDialog type="create" />
 | 
			
		||||
            <ImportAccountDialog />
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </Flex>
 | 
			
		||||
        <Stack
 | 
			
		||||
          css={{
 | 
			
		||||
            flexDirection: 'column',
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            fontSize: '13px',
 | 
			
		||||
            wordWrap: 'break-word',
 | 
			
		||||
            fontWeight: '$body',
 | 
			
		||||
            gap: 0,
 | 
			
		||||
            height: 'calc(100% - 52px)',
 | 
			
		||||
            flexWrap: 'nowrap',
 | 
			
		||||
            overflowY: 'auto'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {snap.accounts.map(account => (
 | 
			
		||||
            <Flex
 | 
			
		||||
              column
 | 
			
		||||
              key={account.address + account.name}
 | 
			
		||||
              onClick={() => setActiveAccountAddress(account.address)}
 | 
			
		||||
              css={{
 | 
			
		||||
                px: '$3',
 | 
			
		||||
                py: props.card ? '$3' : '$2',
 | 
			
		||||
                cursor: 'pointer',
 | 
			
		||||
                borderBottom: props.card ? '1px solid $mauve6' : undefined,
 | 
			
		||||
                '@hover': {
 | 
			
		||||
                  '&:hover': {
 | 
			
		||||
                    background: '$backgroundAlt'
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Flex
 | 
			
		||||
                row
 | 
			
		||||
                css={{
 | 
			
		||||
                  justifyContent: 'space-between'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Box>
 | 
			
		||||
                  <Text>{account.name} </Text>
 | 
			
		||||
                  <Text
 | 
			
		||||
                    css={{
 | 
			
		||||
                      color: '$textMuted',
 | 
			
		||||
                      wordBreak: 'break-word'
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    {account.address}{' '}
 | 
			
		||||
                    {!account?.error ? (
 | 
			
		||||
                      `(${Dinero({
 | 
			
		||||
                        amount: Number(account?.xrp || '0'),
 | 
			
		||||
                        precision: 6
 | 
			
		||||
                      })
 | 
			
		||||
                        .toUnit()
 | 
			
		||||
                        .toLocaleString(undefined, {
 | 
			
		||||
                          style: 'currency',
 | 
			
		||||
                          currency: 'XRP',
 | 
			
		||||
                          currencyDisplay: 'name'
 | 
			
		||||
                        })})`
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      <Box css={{ color: '$red11' }}>
 | 
			
		||||
                        (Account not found, request funds to activate account)
 | 
			
		||||
                      </Box>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </Text>
 | 
			
		||||
                </Box>
 | 
			
		||||
                {!props.hideDeployBtn && (
 | 
			
		||||
                  <div
 | 
			
		||||
                    className="hook-deploy-button"
 | 
			
		||||
                    onClick={e => {
 | 
			
		||||
                      e.stopPropagation()
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <SetHookDialog accountAddress={account.address} />
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
              </Flex>
 | 
			
		||||
              {props.showHookStats && (
 | 
			
		||||
                <Text muted small css={{ mt: '$2' }}>
 | 
			
		||||
                  {account.hooks.length} hook
 | 
			
		||||
                  {account.hooks.length === 1 ? '' : 's'} installed
 | 
			
		||||
                </Text>
 | 
			
		||||
              )}
 | 
			
		||||
            </Flex>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Stack>
 | 
			
		||||
      </Container>
 | 
			
		||||
      <AccountDialog
 | 
			
		||||
        activeAccountAddress={activeAccountAddress}
 | 
			
		||||
        setActiveAccountAddress={setActiveAccountAddress}
 | 
			
		||||
      />
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const transactionsOptions = transactionsData.map(tx => ({
 | 
			
		||||
  value: tx.TransactionType,
 | 
			
		||||
  label: tx.TransactionType
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
const ImportAccountDialog = ({ type = 'import' }: { type?: 'import' | 'create' }) => {
 | 
			
		||||
  const [secret, setSecret] = useState('')
 | 
			
		||||
  const [name, setName] = useState('')
 | 
			
		||||
 | 
			
		||||
  const btnText = type === 'import' ? 'Import' : 'Create'
 | 
			
		||||
  const title = type === 'import' ? 'Import Account' : 'Create Account'
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async () => {
 | 
			
		||||
    if (type === 'create') {
 | 
			
		||||
      const value = capitalize(name)
 | 
			
		||||
      await addFaucetAccount(value, true)
 | 
			
		||||
      setName('')
 | 
			
		||||
      setSecret('')
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    importAccount(secret, name)
 | 
			
		||||
    setName('')
 | 
			
		||||
    setSecret('')
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog>
 | 
			
		||||
      <DialogTrigger asChild>
 | 
			
		||||
        <Button ghost size="sm">
 | 
			
		||||
          {btnText}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </DialogTrigger>
 | 
			
		||||
      <DialogContent aria-describedby={undefined}>
 | 
			
		||||
        <DialogTitle css={{ mb: '$4' }}>{title}</DialogTitle>
 | 
			
		||||
        <Flex column>
 | 
			
		||||
          <Box css={{ mb: '$2' }}>
 | 
			
		||||
            <Label>
 | 
			
		||||
              Account name <Text muted>(optional)</Text>
 | 
			
		||||
            </Label>
 | 
			
		||||
            <Input
 | 
			
		||||
              name="name"
 | 
			
		||||
              type="text"
 | 
			
		||||
              autoComplete="off"
 | 
			
		||||
              autoCapitalize="on"
 | 
			
		||||
              value={name}
 | 
			
		||||
              onChange={e => setName(e.target.value)}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
          {type === 'import' && (
 | 
			
		||||
            <Box>
 | 
			
		||||
              <Label>Account secret</Label>
 | 
			
		||||
              <Input
 | 
			
		||||
                required
 | 
			
		||||
                name="secret"
 | 
			
		||||
                type="password"
 | 
			
		||||
                autoComplete="new-password"
 | 
			
		||||
                value={secret}
 | 
			
		||||
                onChange={e => setSecret(e.target.value)}
 | 
			
		||||
              />
 | 
			
		||||
            </Box>
 | 
			
		||||
          )}
 | 
			
		||||
        </Flex>
 | 
			
		||||
 | 
			
		||||
        <Flex
 | 
			
		||||
          css={{
 | 
			
		||||
            marginTop: 25,
 | 
			
		||||
            justifyContent: 'flex-end',
 | 
			
		||||
            gap: '$3'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <DialogClose asChild>
 | 
			
		||||
            <Button outline>Cancel</Button>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
          <DialogClose asChild>
 | 
			
		||||
            <Button type="submit" variant="primary" onClick={handleSubmit}>
 | 
			
		||||
              {title}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
        </Flex>
 | 
			
		||||
        <DialogClose asChild>
 | 
			
		||||
          <Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
 | 
			
		||||
            <X size="20px" />
 | 
			
		||||
          </Box>
 | 
			
		||||
        </DialogClose>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Accounts
 | 
			
		||||
							
								
								
									
										72
									
								
								components/AlertDialog/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,72 @@
 | 
			
		||||
import { FC, ReactNode } from 'react'
 | 
			
		||||
import { proxy, useSnapshot } from 'valtio'
 | 
			
		||||
import Button from '../Button'
 | 
			
		||||
import Flex from '../Flex'
 | 
			
		||||
import {
 | 
			
		||||
  AlertDialog,
 | 
			
		||||
  AlertDialogAction,
 | 
			
		||||
  AlertDialogCancel,
 | 
			
		||||
  AlertDialogContent,
 | 
			
		||||
  AlertDialogDescription,
 | 
			
		||||
  AlertDialogTitle
 | 
			
		||||
} from './primitive'
 | 
			
		||||
 | 
			
		||||
export interface AlertState {
 | 
			
		||||
  isOpen: boolean
 | 
			
		||||
  title?: string
 | 
			
		||||
  body?: ReactNode
 | 
			
		||||
  cancelText?: string
 | 
			
		||||
  confirmText?: string
 | 
			
		||||
  confirmPrefix?: ReactNode
 | 
			
		||||
  onConfirm?: () => any
 | 
			
		||||
  onCancel?: () => any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const alertState = proxy<AlertState>({
 | 
			
		||||
  isOpen: false
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const Alert: FC = () => {
 | 
			
		||||
  const {
 | 
			
		||||
    title = 'Are you sure?',
 | 
			
		||||
    isOpen,
 | 
			
		||||
    body,
 | 
			
		||||
    cancelText,
 | 
			
		||||
    confirmText = 'Ok',
 | 
			
		||||
    confirmPrefix,
 | 
			
		||||
    onCancel,
 | 
			
		||||
    onConfirm
 | 
			
		||||
  } = useSnapshot(alertState)
 | 
			
		||||
  return (
 | 
			
		||||
    <AlertDialog open={isOpen} onOpenChange={value => (alertState.isOpen = value)}>
 | 
			
		||||
      <AlertDialogContent>
 | 
			
		||||
        <AlertDialogTitle>{title}</AlertDialogTitle>
 | 
			
		||||
        <AlertDialogDescription>{body}</AlertDialogDescription>
 | 
			
		||||
        <Flex css={{ justifyContent: 'flex-end', gap: '$3' }}>
 | 
			
		||||
          {(cancelText || onCancel) && (
 | 
			
		||||
            <AlertDialogCancel asChild>
 | 
			
		||||
              <Button css={{ minWidth: '$16' }} outline onClick={onCancel}>
 | 
			
		||||
                {cancelText || 'Cancel'}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </AlertDialogCancel>
 | 
			
		||||
          )}
 | 
			
		||||
          <AlertDialogAction asChild>
 | 
			
		||||
            <Button
 | 
			
		||||
              css={{ minWidth: '$16' }}
 | 
			
		||||
              variant="primary"
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                await onConfirm?.()
 | 
			
		||||
                alertState.isOpen = false
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {confirmPrefix}
 | 
			
		||||
              {confirmText}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </AlertDialogAction>
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </AlertDialogContent>
 | 
			
		||||
    </AlertDialog>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Alert
 | 
			
		||||
							
								
								
									
										83
									
								
								components/AlertDialog/primitive.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,83 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { blackA } from '@radix-ui/colors'
 | 
			
		||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
 | 
			
		||||
import { styled, keyframes } from '../../stitches.config'
 | 
			
		||||
 | 
			
		||||
const overlayShow = keyframes({
 | 
			
		||||
  '0%': { opacity: 0 },
 | 
			
		||||
  '100%': { opacity: 1 }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const contentShow = keyframes({
 | 
			
		||||
  '0%': { opacity: 0, transform: 'translate(-50%, -48%) scale(.96)' },
 | 
			
		||||
  '100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
 | 
			
		||||
  zIndex: 1000,
 | 
			
		||||
  backgroundColor: blackA.blackA9,
 | 
			
		||||
  position: 'fixed',
 | 
			
		||||
  inset: 0,
 | 
			
		||||
  '@media (prefers-reduced-motion: no-preference)': {
 | 
			
		||||
    animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
 | 
			
		||||
  },
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    backgroundColor: blackA.blackA11
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const Root: React.FC<AlertDialogPrimitive.AlertDialogProps> = ({ children, ...rest }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <AlertDialogPrimitive.Root {...rest}>
 | 
			
		||||
      <StyledOverlay />
 | 
			
		||||
      {children}
 | 
			
		||||
    </AlertDialogPrimitive.Root>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledContent = styled(AlertDialogPrimitive.Content, {
 | 
			
		||||
  zIndex: 1000,
 | 
			
		||||
  backgroundColor: '$mauve2',
 | 
			
		||||
  color: '$mauve12',
 | 
			
		||||
  borderRadius: '$md',
 | 
			
		||||
  boxShadow: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)',
 | 
			
		||||
  position: 'fixed',
 | 
			
		||||
  top: '50%',
 | 
			
		||||
  left: '50%',
 | 
			
		||||
  transform: 'translate(-50%, -50%)',
 | 
			
		||||
  width: '90vw',
 | 
			
		||||
  maxWidth: '450px',
 | 
			
		||||
  maxHeight: '85vh',
 | 
			
		||||
  padding: 25,
 | 
			
		||||
  '@media (prefers-reduced-motion: no-preference)': {
 | 
			
		||||
    animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
 | 
			
		||||
  },
 | 
			
		||||
  '&:focus': { outline: 'none' },
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    backgroundColor: '$mauve5',
 | 
			
		||||
    boxShadow: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledTitle = styled(AlertDialogPrimitive.Title, {
 | 
			
		||||
  margin: 0,
 | 
			
		||||
  color: '$mauve12',
 | 
			
		||||
  fontWeight: 500,
 | 
			
		||||
  fontSize: '$lg'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledDescription = styled(AlertDialogPrimitive.Description, {
 | 
			
		||||
  marginBottom: 20,
 | 
			
		||||
  color: '$mauve11',
 | 
			
		||||
  lineHeight: 1.5,
 | 
			
		||||
  fontSize: '$md'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
export const AlertDialog = Root
 | 
			
		||||
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger
 | 
			
		||||
export const AlertDialogContent = StyledContent
 | 
			
		||||
export const AlertDialogTitle = StyledTitle
 | 
			
		||||
export const AlertDialogDescription = StyledDescription
 | 
			
		||||
export const AlertDialogAction = AlertDialogPrimitive.Action
 | 
			
		||||
export const AlertDialogCancel = AlertDialogPrimitive.Cancel
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const Box = styled("div", {
 | 
			
		||||
const Box = styled('div', {
 | 
			
		||||
  // all: "unset",
 | 
			
		||||
  boxSizing: "border-box",
 | 
			
		||||
});
 | 
			
		||||
  boxSizing: 'border-box'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Box;
 | 
			
		||||
export default Box
 | 
			
		||||
 
 | 
			
		||||
@@ -1,171 +1,285 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import Flex from './Flex'
 | 
			
		||||
import Spinner from './Spinner'
 | 
			
		||||
 | 
			
		||||
const Button = styled("button", {
 | 
			
		||||
export const StyledButton = styled('button', {
 | 
			
		||||
  // Reset
 | 
			
		||||
  all: "unset",
 | 
			
		||||
  appereance: "none",
 | 
			
		||||
  fontFamily: "$body",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  boxSizing: "border-box",
 | 
			
		||||
  userSelect: "none",
 | 
			
		||||
  "&::before": {
 | 
			
		||||
    boxSizing: "border-box",
 | 
			
		||||
  all: 'unset',
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  appereance: 'none',
 | 
			
		||||
  fontFamily: '$body',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  boxSizing: 'border-box',
 | 
			
		||||
  userSelect: 'none',
 | 
			
		||||
  '&::before': {
 | 
			
		||||
    boxSizing: 'border-box'
 | 
			
		||||
  },
 | 
			
		||||
  "&::after": {
 | 
			
		||||
    boxSizing: "border-box",
 | 
			
		||||
  '&::after': {
 | 
			
		||||
    boxSizing: 'border-box'
 | 
			
		||||
  },
 | 
			
		||||
  // Custom reset?
 | 
			
		||||
  display: "inline-flex",
 | 
			
		||||
  display: 'inline-flex',
 | 
			
		||||
  flexShrink: 0,
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  lineHeight: "1",
 | 
			
		||||
  gap: "5px",
 | 
			
		||||
  WebkitTapHighlightColor: "rgba(0,0,0,0)",
 | 
			
		||||
  justifyContent: 'center',
 | 
			
		||||
  lineHeight: '1',
 | 
			
		||||
  gap: '5px',
 | 
			
		||||
  WebkitTapHighlightColor: 'rgba(0,0,0,0)',
 | 
			
		||||
  // Custom
 | 
			
		||||
  height: "$6",
 | 
			
		||||
  px: "$2",
 | 
			
		||||
  fontSize: "$2",
 | 
			
		||||
  height: '$6',
 | 
			
		||||
  px: '$2',
 | 
			
		||||
  fontSize: '$2',
 | 
			
		||||
  fontWeight: 500,
 | 
			
		||||
  fontVariantNumeric: "tabular-nums",
 | 
			
		||||
  backgroundColor: "red",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  "&:disabled": {
 | 
			
		||||
    opacity: 0.8,
 | 
			
		||||
    pointerEvents: "none",
 | 
			
		||||
  fontVariantNumeric: 'tabular-nums',
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
  width: 'max-content',
 | 
			
		||||
  '&:disabled': {
 | 
			
		||||
    opacity: 0.6,
 | 
			
		||||
    pointerEvents: 'none',
 | 
			
		||||
    cursor: 'not-allowed'
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    size: {
 | 
			
		||||
      xs: {
 | 
			
		||||
        borderRadius: '$sm',
 | 
			
		||||
        height: '$5',
 | 
			
		||||
        px: '$2',
 | 
			
		||||
        fontSize: '$xs'
 | 
			
		||||
      },
 | 
			
		||||
      sm: {
 | 
			
		||||
        borderRadius: "$sm",
 | 
			
		||||
        height: "$7",
 | 
			
		||||
        px: "$3",
 | 
			
		||||
        fontSize: "$xs",
 | 
			
		||||
        borderRadius: '$sm',
 | 
			
		||||
        height: '$7',
 | 
			
		||||
        px: '$3',
 | 
			
		||||
        fontSize: '$xs'
 | 
			
		||||
      },
 | 
			
		||||
      md: {
 | 
			
		||||
        borderRadius: "$sm",
 | 
			
		||||
        height: "$8",
 | 
			
		||||
        px: "$3",
 | 
			
		||||
        fontSize: "$xs",
 | 
			
		||||
        borderRadius: '$sm',
 | 
			
		||||
        height: '$8',
 | 
			
		||||
        px: '$3',
 | 
			
		||||
        fontSize: '$xs'
 | 
			
		||||
      },
 | 
			
		||||
      lg: {
 | 
			
		||||
        borderRadius: "$sm",
 | 
			
		||||
        height: "$10",
 | 
			
		||||
        px: "$4",
 | 
			
		||||
        fontSize: "$xs",
 | 
			
		||||
      },
 | 
			
		||||
        borderRadius: '$sm',
 | 
			
		||||
        height: '$10',
 | 
			
		||||
        px: '$4',
 | 
			
		||||
        fontSize: '$xs'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    variant: {
 | 
			
		||||
      link: {
 | 
			
		||||
        textDecoration: 'underline',
 | 
			
		||||
        fontSize: 'inherit',
 | 
			
		||||
        color: '$textMuted',
 | 
			
		||||
        textUnderlineOffset: '2px'
 | 
			
		||||
      },
 | 
			
		||||
      default: {
 | 
			
		||||
        backgroundColor: "$slate12",
 | 
			
		||||
        boxShadow: "inset 0 0 0 1px $colors$slate12",
 | 
			
		||||
        color: "$slate1",
 | 
			
		||||
        "@hover": {
 | 
			
		||||
          "&:hover": {
 | 
			
		||||
            backgroundColor: "$slate12",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$slate12",
 | 
			
		||||
          },
 | 
			
		||||
        backgroundColor: '$mauve12',
 | 
			
		||||
        boxShadow: 'inset 0 0 0 1px $colors$mauve12',
 | 
			
		||||
        color: '$mauve1',
 | 
			
		||||
        '@hover': {
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            backgroundColor: '$mauve12',
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$mauve12'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "&:active": {
 | 
			
		||||
          backgroundColor: "$slate10",
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$slate11",
 | 
			
		||||
        '&:active': {
 | 
			
		||||
          backgroundColor: '$mauve10',
 | 
			
		||||
          boxShadow: 'inset 0 0 0 1px $colors$mauve11'
 | 
			
		||||
        },
 | 
			
		||||
        "&:focus": {
 | 
			
		||||
          boxShadow:
 | 
			
		||||
            "inset 0 0 0 1px $colors$slate12, 0 0 0 1px $colors$slate12",
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          boxShadow: 'inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12'
 | 
			
		||||
        },
 | 
			
		||||
        '&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
 | 
			
		||||
          {
 | 
			
		||||
            backgroundColor: "$slate4",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$slate8",
 | 
			
		||||
          },
 | 
			
		||||
            backgroundColor: '$mauve4',
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$mauve8'
 | 
			
		||||
          }
 | 
			
		||||
      },
 | 
			
		||||
      primary: {
 | 
			
		||||
        backgroundColor: `$pink9`,
 | 
			
		||||
        boxShadow: "inset 0 0 0 1px $colors$pink9",
 | 
			
		||||
        color: "$white",
 | 
			
		||||
        "@hover": {
 | 
			
		||||
          "&:hover": {
 | 
			
		||||
            backgroundColor: "$pink10",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$pink11",
 | 
			
		||||
          },
 | 
			
		||||
        backgroundColor: `$accent`,
 | 
			
		||||
        boxShadow: 'inset 0 0 0 1px $colors$purple9',
 | 
			
		||||
        color: '$white',
 | 
			
		||||
        '@hover': {
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            backgroundColor: '$purple10',
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$purple11'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "&:active": {
 | 
			
		||||
          backgroundColor: "$pink8",
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$pink8",
 | 
			
		||||
        '&:active': {
 | 
			
		||||
          backgroundColor: '$purple8',
 | 
			
		||||
          boxShadow: 'inset 0 0 0 1px $colors$purple8'
 | 
			
		||||
        },
 | 
			
		||||
        "&:focus": {
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$pink8",
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          boxShadow: 'inset 0 0 0 2px $colors$purple12'
 | 
			
		||||
        },
 | 
			
		||||
        '&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
 | 
			
		||||
          {
 | 
			
		||||
            backgroundColor: "$slate4",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$pink8",
 | 
			
		||||
          },
 | 
			
		||||
            backgroundColor: '$mauve4',
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$purple8'
 | 
			
		||||
          }
 | 
			
		||||
      },
 | 
			
		||||
      secondary: {
 | 
			
		||||
        backgroundColor: `$purple9`,
 | 
			
		||||
        boxShadow: 'inset 0 0 0 1px $colors$purple9',
 | 
			
		||||
        color: '$white',
 | 
			
		||||
        '@hover': {
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            backgroundColor: '$purple10',
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$purple11'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        '&:active': {
 | 
			
		||||
          backgroundColor: '$purple8',
 | 
			
		||||
          boxShadow: 'inset 0 0 0 1px $colors$purple8'
 | 
			
		||||
        },
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          boxShadow: 'inset 0 0 0 2px $colors$purple12'
 | 
			
		||||
        },
 | 
			
		||||
        '&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
 | 
			
		||||
          {
 | 
			
		||||
            backgroundColor: '$mauve4',
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$purple8'
 | 
			
		||||
          }
 | 
			
		||||
      },
 | 
			
		||||
      destroy: {
 | 
			
		||||
        backgroundColor: `$red9`,
 | 
			
		||||
        boxShadow: 'inset 0 0 0 1px $colors$red9',
 | 
			
		||||
        color: '$white',
 | 
			
		||||
        '@hover': {
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            backgroundColor: '$red10',
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$red11'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        '&:active': {
 | 
			
		||||
          backgroundColor: '$red8',
 | 
			
		||||
          boxShadow: 'inset 0 0 0 1px $colors$red8'
 | 
			
		||||
        },
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          boxShadow: 'inset 0 0 0 2px $colors$red12'
 | 
			
		||||
        },
 | 
			
		||||
        '&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
 | 
			
		||||
          {
 | 
			
		||||
            backgroundColor: '$mauve4',
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$red8'
 | 
			
		||||
          }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    muted: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: '$textMuted'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    isDisabled: {
 | 
			
		||||
      true: {
 | 
			
		||||
        opacity: 0.6,
 | 
			
		||||
        // pointerEvents: "none",
 | 
			
		||||
        cursor: 'auto',
 | 
			
		||||
        '&:hover': {
 | 
			
		||||
          boxShadow: 'inherit'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    outline: {
 | 
			
		||||
      true: {
 | 
			
		||||
        backgroundColor: "transparent",
 | 
			
		||||
      },
 | 
			
		||||
        backgroundColor: 'transparent'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    uppercase: {
 | 
			
		||||
      true: {
 | 
			
		||||
        textTransform: "uppercase",
 | 
			
		||||
      },
 | 
			
		||||
        textTransform: 'uppercase'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    fullWidth: {
 | 
			
		||||
      true: {
 | 
			
		||||
        width: '100%'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    ghost: {
 | 
			
		||||
      true: {
 | 
			
		||||
        boxShadow: "none",
 | 
			
		||||
        background: "transparent",
 | 
			
		||||
        color: "$slate12",
 | 
			
		||||
        "@hover": {
 | 
			
		||||
          "&:hover": {
 | 
			
		||||
            backgroundColor: "$slate6",
 | 
			
		||||
            boxShadow: "none",
 | 
			
		||||
          },
 | 
			
		||||
        boxShadow: 'none',
 | 
			
		||||
        background: 'transparent',
 | 
			
		||||
        color: '$mauve12',
 | 
			
		||||
        '@hover': {
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            backgroundColor: '$mauve6',
 | 
			
		||||
            boxShadow: 'none'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "&:active": {
 | 
			
		||||
          backgroundColor: "$slate8",
 | 
			
		||||
          boxShadow: "none",
 | 
			
		||||
        '&:active': {
 | 
			
		||||
          backgroundColor: '$mauve8',
 | 
			
		||||
          boxShadow: 'none'
 | 
			
		||||
        },
 | 
			
		||||
        "&:focus": {
 | 
			
		||||
          boxShadow: "none",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          boxShadow: 'none'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    isLoading: {
 | 
			
		||||
      true: {
 | 
			
		||||
        '& .button-content': {
 | 
			
		||||
          visibility: 'hidden'
 | 
			
		||||
        },
 | 
			
		||||
        pointerEvents: 'none'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  compoundVariants: [
 | 
			
		||||
    {
 | 
			
		||||
      outline: true,
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      variant: 'default',
 | 
			
		||||
      css: {
 | 
			
		||||
        background: "transparent",
 | 
			
		||||
        color: "$slate12",
 | 
			
		||||
        boxShadow: "inset 0 0 0 1px $colors$slate10",
 | 
			
		||||
        "&:hover": {
 | 
			
		||||
          color: "$slate12",
 | 
			
		||||
          background: "$slate5",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
        background: 'transparent',
 | 
			
		||||
        color: '$mauve12',
 | 
			
		||||
        boxShadow: 'inset 0 0 0 1px $colors$mauve10',
 | 
			
		||||
        '&:hover': {
 | 
			
		||||
          color: '$mauve12',
 | 
			
		||||
          background: '$mauve5'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      outline: true,
 | 
			
		||||
      variant: "primary",
 | 
			
		||||
      variant: 'primary',
 | 
			
		||||
      css: {
 | 
			
		||||
        background: "transparent",
 | 
			
		||||
        color: "$slate12",
 | 
			
		||||
        "&:hover": {
 | 
			
		||||
          color: "$slate12",
 | 
			
		||||
          background: "$slate5",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
        background: 'transparent',
 | 
			
		||||
        color: '$mauve12',
 | 
			
		||||
        '&:hover': {
 | 
			
		||||
          color: '$mauve12',
 | 
			
		||||
          background: '$mauve5'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      outline: true,
 | 
			
		||||
      variant: 'secondary',
 | 
			
		||||
      css: {
 | 
			
		||||
        background: 'transparent',
 | 
			
		||||
        color: '$mauve12',
 | 
			
		||||
        '&:hover': {
 | 
			
		||||
          color: '$mauve12',
 | 
			
		||||
          background: '$mauve5'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  defaultVariants: {
 | 
			
		||||
    size: "md",
 | 
			
		||||
    variant: "default",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
    size: 'md',
 | 
			
		||||
    variant: 'default'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Button;
 | 
			
		||||
const CustomButton: React.FC<React.ComponentProps<typeof StyledButton> & { as?: string }> =
 | 
			
		||||
  React.forwardRef(({ children, as = 'button', ...rest }, ref) => (
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    <StyledButton {...rest} ref={ref} as={as}>
 | 
			
		||||
      <Flex as="span" css={{ gap: '$2', alignItems: 'center' }} className="button-content">
 | 
			
		||||
        {children}
 | 
			
		||||
      </Flex>
 | 
			
		||||
      {rest.isLoading && <Spinner css={{ position: 'absolute' }} />}
 | 
			
		||||
    </StyledButton>
 | 
			
		||||
  ))
 | 
			
		||||
 | 
			
		||||
CustomButton.displayName = 'CustomButton'
 | 
			
		||||
 | 
			
		||||
export default CustomButton
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								components/ButtonGroup.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import { StyledButton } from './Button'
 | 
			
		||||
 | 
			
		||||
const ButtonGroup = styled('div', {
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  marginLeft: '1px',
 | 
			
		||||
  [`& ${StyledButton}`]: {
 | 
			
		||||
    marginLeft: '-1px',
 | 
			
		||||
    px: '$4',
 | 
			
		||||
    zIndex: 2,
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    '&:hover, &:focus': {
 | 
			
		||||
      zIndex: 200
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  [`& ${StyledButton}:not(:only-of-type):not(:first-child):not(:last-child)`]: {
 | 
			
		||||
    borderRadius: 0
 | 
			
		||||
  },
 | 
			
		||||
  [`& ${StyledButton}:first-child:not(:only-of-type)`]: {
 | 
			
		||||
    borderBottomRightRadius: 0,
 | 
			
		||||
    borderTopRightRadius: 0
 | 
			
		||||
  },
 | 
			
		||||
  [`& ${StyledButton}:last-child:not(:only-of-type)`]: {
 | 
			
		||||
    borderBottomLeftRadius: 0,
 | 
			
		||||
    borderTopLeftRadius: 0
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default ButtonGroup
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import Box from './Box'
 | 
			
		||||
 | 
			
		||||
const Container = styled(Box, {
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  marginLeft: "auto",
 | 
			
		||||
  marginRight: "auto",
 | 
			
		||||
  px: "$4",
 | 
			
		||||
  maxWidth: "100%",
 | 
			
		||||
});
 | 
			
		||||
  width: '100%',
 | 
			
		||||
  marginLeft: 'auto',
 | 
			
		||||
  marginRight: 'auto',
 | 
			
		||||
  px: '$4',
 | 
			
		||||
  maxWidth: '100%'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Container;
 | 
			
		||||
export default Container
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								components/ContextMenu/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,121 @@
 | 
			
		||||
import { CaretRight, Check, Circle } from 'phosphor-react'
 | 
			
		||||
import { FC, Fragment, ReactNode } from 'react'
 | 
			
		||||
import { Flex, Text } from '../'
 | 
			
		||||
import {
 | 
			
		||||
  ContextMenuCheckboxItem,
 | 
			
		||||
  ContextMenuContent,
 | 
			
		||||
  ContextMenuItem,
 | 
			
		||||
  ContextMenuItemIndicator,
 | 
			
		||||
  ContextMenuLabel,
 | 
			
		||||
  ContextMenuRadioGroup,
 | 
			
		||||
  ContextMenuRadioItem,
 | 
			
		||||
  ContextMenuRoot,
 | 
			
		||||
  ContextMenuSeparator,
 | 
			
		||||
  ContextMenuTrigger,
 | 
			
		||||
  ContextMenuTriggerItem
 | 
			
		||||
} from './primitive'
 | 
			
		||||
 | 
			
		||||
export type TextOption = {
 | 
			
		||||
  type: 'text'
 | 
			
		||||
  label: ReactNode
 | 
			
		||||
  onSelect?: () => any
 | 
			
		||||
  children?: ContentMenuOption[]
 | 
			
		||||
}
 | 
			
		||||
export type SeparatorOption = { type: 'separator' }
 | 
			
		||||
export type CheckboxOption = {
 | 
			
		||||
  type: 'checkbox'
 | 
			
		||||
  label: ReactNode
 | 
			
		||||
  checked?: boolean
 | 
			
		||||
  onCheckedChange?: (isChecked: boolean) => any
 | 
			
		||||
}
 | 
			
		||||
export type RadioOption<T extends string = string> = {
 | 
			
		||||
  type: 'radio'
 | 
			
		||||
  label: ReactNode
 | 
			
		||||
  onValueChange?: (value: string) => any
 | 
			
		||||
  value: T
 | 
			
		||||
  options?: { value: T; label?: ReactNode }[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type WithCommons = { key: string; disabled?: boolean }
 | 
			
		||||
 | 
			
		||||
export type ContentMenuOption = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
 | 
			
		||||
  WithCommons
 | 
			
		||||
 | 
			
		||||
export interface IContextMenu {
 | 
			
		||||
  options?: ContentMenuOption[]
 | 
			
		||||
  isNested?: boolean
 | 
			
		||||
}
 | 
			
		||||
export const ContextMenu: FC<IContextMenu> = ({ children, options, isNested }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <ContextMenuRoot>
 | 
			
		||||
      {isNested ? (
 | 
			
		||||
        <ContextMenuTriggerItem>{children}</ContextMenuTriggerItem>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <ContextMenuTrigger>{children}</ContextMenuTrigger>
 | 
			
		||||
      )}
 | 
			
		||||
      {options && !!options.length && (
 | 
			
		||||
        <ContextMenuContent sideOffset={isNested ? 2 : 5}>
 | 
			
		||||
          {options.map(({ key, ...option }) => {
 | 
			
		||||
            if (option.type === 'text') {
 | 
			
		||||
              const { children, label, onSelect } = option
 | 
			
		||||
              if (children)
 | 
			
		||||
                return (
 | 
			
		||||
                  <ContextMenu isNested key={key} options={children}>
 | 
			
		||||
                    <Flex fluid row justify="space-between" align="center">
 | 
			
		||||
                      <Text>{label}</Text>
 | 
			
		||||
                      <CaretRight />
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                  </ContextMenu>
 | 
			
		||||
                )
 | 
			
		||||
              return (
 | 
			
		||||
                <ContextMenuItem key={key} onSelect={onSelect}>
 | 
			
		||||
                  {label}
 | 
			
		||||
                </ContextMenuItem>
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
            if (option.type === 'checkbox') {
 | 
			
		||||
              const { label, checked, onCheckedChange } = option
 | 
			
		||||
              return (
 | 
			
		||||
                <ContextMenuCheckboxItem
 | 
			
		||||
                  key={key}
 | 
			
		||||
                  checked={checked}
 | 
			
		||||
                  onCheckedChange={onCheckedChange}
 | 
			
		||||
                >
 | 
			
		||||
                  <Flex row align="center">
 | 
			
		||||
                    <ContextMenuItemIndicator>
 | 
			
		||||
                      <Check />
 | 
			
		||||
                    </ContextMenuItemIndicator>
 | 
			
		||||
                    <Text css={{ ml: checked ? '$4' : undefined }}>{label}</Text>
 | 
			
		||||
                  </Flex>
 | 
			
		||||
                </ContextMenuCheckboxItem>
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
            if (option.type === 'radio') {
 | 
			
		||||
              const { label, options, onValueChange, value } = option
 | 
			
		||||
              return (
 | 
			
		||||
                <Fragment key={key}>
 | 
			
		||||
                  <ContextMenuLabel>{label}</ContextMenuLabel>
 | 
			
		||||
                  <ContextMenuRadioGroup value={value} onValueChange={onValueChange}>
 | 
			
		||||
                    {options?.map(({ value: v, label }) => {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <ContextMenuRadioItem key={v} value={v}>
 | 
			
		||||
                          <ContextMenuItemIndicator>
 | 
			
		||||
                            <Circle weight="fill" />
 | 
			
		||||
                          </ContextMenuItemIndicator>
 | 
			
		||||
                          <Text css={{ ml: '$4' }}>{label}</Text>
 | 
			
		||||
                        </ContextMenuRadioItem>
 | 
			
		||||
                      )
 | 
			
		||||
                    })}
 | 
			
		||||
                  </ContextMenuRadioGroup>
 | 
			
		||||
                </Fragment>
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
            return <ContextMenuSeparator key={key} />
 | 
			
		||||
          })}
 | 
			
		||||
        </ContextMenuContent>
 | 
			
		||||
      )}
 | 
			
		||||
    </ContextMenuRoot>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ContextMenu
 | 
			
		||||
							
								
								
									
										107
									
								
								components/ContextMenu/primitive.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,107 @@
 | 
			
		||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
 | 
			
		||||
import { styled } from '../../stitches.config'
 | 
			
		||||
import {
 | 
			
		||||
  slideDownAndFade,
 | 
			
		||||
  slideLeftAndFade,
 | 
			
		||||
  slideRightAndFade,
 | 
			
		||||
  slideUpAndFade
 | 
			
		||||
} from '../../styles/keyframes'
 | 
			
		||||
 | 
			
		||||
const StyledContent = styled(ContextMenuPrimitive.Content, {
 | 
			
		||||
  minWidth: 140,
 | 
			
		||||
  backgroundColor: '$backgroundOverlay',
 | 
			
		||||
  borderRadius: 6,
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  padding: '5px',
 | 
			
		||||
  boxShadow:
 | 
			
		||||
    '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
 | 
			
		||||
  '@media (prefers-reduced-motion: no-preference)': {
 | 
			
		||||
    animationDuration: '400ms',
 | 
			
		||||
    animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
 | 
			
		||||
    willChange: 'transform, opacity',
 | 
			
		||||
    '&[data-state="open"]': {
 | 
			
		||||
      '&[data-side="top"]': { animationName: slideDownAndFade },
 | 
			
		||||
      '&[data-side="right"]': { animationName: slideLeftAndFade },
 | 
			
		||||
      '&[data-side="bottom"]': { animationName: slideUpAndFade },
 | 
			
		||||
      '&[data-side="left"]': { animationName: slideRightAndFade }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      '0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const itemStyles = {
 | 
			
		||||
  all: 'unset',
 | 
			
		||||
  fontSize: 13,
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: '$text',
 | 
			
		||||
  borderRadius: 3,
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  height: 28,
 | 
			
		||||
  padding: '0 7px',
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  paddingLeft: 10,
 | 
			
		||||
  userSelect: 'none',
 | 
			
		||||
 | 
			
		||||
  '&[data-disabled]': {
 | 
			
		||||
    color: '$textMuted',
 | 
			
		||||
    pointerEvents: 'none'
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  '&:focus': {
 | 
			
		||||
    backgroundColor: '$purple9',
 | 
			
		||||
    color: '$white'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles })
 | 
			
		||||
const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, {
 | 
			
		||||
  ...itemStyles
 | 
			
		||||
})
 | 
			
		||||
const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, {
 | 
			
		||||
  ...itemStyles
 | 
			
		||||
})
 | 
			
		||||
const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, {
 | 
			
		||||
  '&[data-state="open"]': {
 | 
			
		||||
    backgroundColor: '$purple9',
 | 
			
		||||
    color: '$purple9'
 | 
			
		||||
  },
 | 
			
		||||
  ...itemStyles
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledLabel = styled(ContextMenuPrimitive.Label, {
 | 
			
		||||
  paddingLeft: 10,
 | 
			
		||||
  fontSize: 12,
 | 
			
		||||
  lineHeight: '25px',
 | 
			
		||||
  color: '$text'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledSeparator = styled(ContextMenuPrimitive.Separator, {
 | 
			
		||||
  height: 1,
 | 
			
		||||
  backgroundColor: '$backgroundAlt',
 | 
			
		||||
  margin: 5
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledItemIndicator = styled(ContextMenuPrimitive.ItemIndicator, {
 | 
			
		||||
  position: 'absolute',
 | 
			
		||||
  left: 0,
 | 
			
		||||
  width: 25,
 | 
			
		||||
  display: 'inline-flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'center'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const ContextMenuRoot = ContextMenuPrimitive.Root
 | 
			
		||||
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger
 | 
			
		||||
export const ContextMenuContent = StyledContent
 | 
			
		||||
export const ContextMenuItem = StyledItem
 | 
			
		||||
export const ContextMenuCheckboxItem = StyledCheckboxItem
 | 
			
		||||
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
 | 
			
		||||
export const ContextMenuRadioItem = StyledRadioItem
 | 
			
		||||
export const ContextMenuItemIndicator = StyledItemIndicator
 | 
			
		||||
export const ContextMenuTriggerItem = StyledTriggerItem
 | 
			
		||||
export const ContextMenuLabel = StyledLabel
 | 
			
		||||
export const ContextMenuSeparator = StyledSeparator
 | 
			
		||||
							
								
								
									
										182
									
								
								components/DebugStream.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,182 @@
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
import ReconnectingWebSocket, { CloseEvent } from 'reconnecting-websocket'
 | 
			
		||||
import { proxy, ref, useSnapshot } from 'valtio'
 | 
			
		||||
import { subscribeKey } from 'valtio/utils'
 | 
			
		||||
import { Select } from '.'
 | 
			
		||||
import state, { ILog, transactionsState } from '../state'
 | 
			
		||||
import { extractJSON } from '../utils/json'
 | 
			
		||||
import EnrichLog from './EnrichLog'
 | 
			
		||||
import LogBox from './LogBox'
 | 
			
		||||
 | 
			
		||||
interface ISelect<T = string> {
 | 
			
		||||
  label: string
 | 
			
		||||
  value: T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IStreamState {
 | 
			
		||||
  selectedAccount: ISelect | null
 | 
			
		||||
  status: 'idle' | 'opened' | 'closed'
 | 
			
		||||
  statusChangeTimestamp?: number
 | 
			
		||||
  logs: ILog[]
 | 
			
		||||
  socket?: ReconnectingWebSocket
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const streamState = proxy<IStreamState>({
 | 
			
		||||
  selectedAccount: null as ISelect | null,
 | 
			
		||||
  status: 'idle',
 | 
			
		||||
  logs: [] as ILog[]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const onOpen = (account: ISelect | null) => {
 | 
			
		||||
  if (!account) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  // streamState.logs = [];
 | 
			
		||||
  streamState.status = 'opened'
 | 
			
		||||
  streamState.statusChangeTimestamp = Date.now()
 | 
			
		||||
 | 
			
		||||
  pushLog(`Debug stream opened for account ${account?.value}`, {
 | 
			
		||||
    type: 'success'
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
const onError = () => {
 | 
			
		||||
  pushLog('Something went wrong! Check your connection and try again.', {
 | 
			
		||||
    type: 'error'
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
const onClose = (e: CloseEvent) => {
 | 
			
		||||
  // 999 = closed websocket connection by switching account
 | 
			
		||||
  if (e.code !== 4999) {
 | 
			
		||||
    pushLog(`Connection was closed. [code: ${e.code}]`, {
 | 
			
		||||
      type: 'error'
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  streamState.status = 'closed'
 | 
			
		||||
  streamState.statusChangeTimestamp = Date.now()
 | 
			
		||||
}
 | 
			
		||||
const onMessage = (event: any) => {
 | 
			
		||||
  // Ping returns just account address, if we get that
 | 
			
		||||
  // response we don't need to log anything
 | 
			
		||||
  if (event.data !== streamState.selectedAccount?.value) {
 | 
			
		||||
    pushLog(event.data)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let interval: NodeJS.Timer | null = null
 | 
			
		||||
 | 
			
		||||
const addListeners = (account: ISelect | null) => {
 | 
			
		||||
  if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  streamState.logs = []
 | 
			
		||||
  if (account?.value) {
 | 
			
		||||
    if (interval) {
 | 
			
		||||
      clearInterval(interval)
 | 
			
		||||
    }
 | 
			
		||||
    if (streamState.socket) {
 | 
			
		||||
      streamState.socket?.removeEventListener('open', () => onOpen(account))
 | 
			
		||||
      streamState.socket?.removeEventListener('close', onClose)
 | 
			
		||||
      streamState.socket?.removeEventListener('error', onError)
 | 
			
		||||
      streamState.socket?.removeEventListener('message', onMessage)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    streamState.socket = ref(
 | 
			
		||||
      new ReconnectingWebSocket(
 | 
			
		||||
        `wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
    if (streamState.socket) {
 | 
			
		||||
      interval = setInterval(() => {
 | 
			
		||||
        streamState.socket?.send('')
 | 
			
		||||
      }, 45000)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    streamState.socket.addEventListener('open', () => onOpen(account))
 | 
			
		||||
    streamState.socket.addEventListener('close', onClose)
 | 
			
		||||
    streamState.socket.addEventListener('error', onError)
 | 
			
		||||
    streamState.socket.addEventListener('message', onMessage)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
subscribeKey(streamState, 'selectedAccount', addListeners)
 | 
			
		||||
 | 
			
		||||
const clearLog = () => {
 | 
			
		||||
  streamState.logs = []
 | 
			
		||||
  streamState.statusChangeTimestamp = Date.now()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DebugStream = () => {
 | 
			
		||||
  const { selectedAccount, logs } = useSnapshot(streamState)
 | 
			
		||||
  const { activeHeader: activeTxTab } = useSnapshot(transactionsState)
 | 
			
		||||
  const { accounts } = useSnapshot(state)
 | 
			
		||||
 | 
			
		||||
  const accountOptions = accounts.map(acc => ({
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
    value: acc.address
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  const renderNav = () => (
 | 
			
		||||
    <>
 | 
			
		||||
      <Select
 | 
			
		||||
        instanceId="DSAccount"
 | 
			
		||||
        placeholder="Select account"
 | 
			
		||||
        options={accountOptions}
 | 
			
		||||
        hideSelectedOptions
 | 
			
		||||
        value={selectedAccount}
 | 
			
		||||
        onChange={acc => {
 | 
			
		||||
          streamState.socket?.close(4999, 'Old connection closed because user switched account')
 | 
			
		||||
          streamState.selectedAccount = acc as any
 | 
			
		||||
        }}
 | 
			
		||||
        css={{ width: '100%' }}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const account = transactionsState.transactions.find(tx => tx.header === activeTxTab)?.state
 | 
			
		||||
      .selectedAccount
 | 
			
		||||
 | 
			
		||||
    if (account && account.value !== streamState.selectedAccount?.value)
 | 
			
		||||
      streamState.selectedAccount = account
 | 
			
		||||
  }, [activeTxTab])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <LogBox enhanced renderNav={renderNav} title="Debug stream" logs={logs} clearLog={clearLog} />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default DebugStream
 | 
			
		||||
 | 
			
		||||
export const pushLog = (str: any, opts: Partial<Pick<ILog, 'type'>> = {}): ILog | undefined => {
 | 
			
		||||
  if (!str) return
 | 
			
		||||
  if (typeof str !== 'string') throw Error('Unrecognized debug log stream!')
 | 
			
		||||
 | 
			
		||||
  const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m)
 | 
			
		||||
  const [_, tm, msg] = match || []
 | 
			
		||||
 | 
			
		||||
  const timestamp = Date.parse(tm || '') || undefined
 | 
			
		||||
  const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString()
 | 
			
		||||
 | 
			
		||||
  const extracted = extractJSON(msg)
 | 
			
		||||
  const _message = !extracted ? msg : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1)
 | 
			
		||||
  const message = ref(<EnrichLog str={_message} />)
 | 
			
		||||
 | 
			
		||||
  const _jsonData = extracted ? JSON.stringify(extracted.result, null, 2) : undefined
 | 
			
		||||
  const jsonData = _jsonData ? ref(<EnrichLog str={_jsonData} />) : undefined
 | 
			
		||||
 | 
			
		||||
  if (extracted?.result?.id?._Request?.includes('hooks-builder-req')) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { type = 'log' } = opts
 | 
			
		||||
  const log: ILog = {
 | 
			
		||||
    type,
 | 
			
		||||
    message,
 | 
			
		||||
    timestring,
 | 
			
		||||
    jsonData,
 | 
			
		||||
    defaultCollapsed: true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (log) streamState.logs.push(log)
 | 
			
		||||
  return log
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										166
									
								
								components/DeployEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,166 @@
 | 
			
		||||
import React, { useState } from 'react'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
 | 
			
		||||
import { useTheme } from 'next-themes'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import NextLink from 'next/link'
 | 
			
		||||
import ReactTimeAgo from 'react-time-ago'
 | 
			
		||||
import filesize from 'filesize'
 | 
			
		||||
 | 
			
		||||
import Box from './Box'
 | 
			
		||||
import Container from './Container'
 | 
			
		||||
import state from '../state'
 | 
			
		||||
import wat from '../utils/wat-highlight'
 | 
			
		||||
 | 
			
		||||
import EditorNavigation from './EditorNavigation'
 | 
			
		||||
import { Button, Text, Link, Flex, Tabs, Tab } from '.'
 | 
			
		||||
import Monaco from './Monaco'
 | 
			
		||||
 | 
			
		||||
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024]
 | 
			
		||||
 | 
			
		||||
const DeployEditor = () => {
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
  const [showContent, setShowContent] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const compiledFiles = snap.files.filter(file => file.compiledContent)
 | 
			
		||||
  const activeFile = compiledFiles[snap.activeWat]
 | 
			
		||||
 | 
			
		||||
  const renderNav = () => (
 | 
			
		||||
    <Tabs activeIndex={snap.activeWat} onChangeActive={idx => (state.activeWat = idx)}>
 | 
			
		||||
      {compiledFiles.map((file, index) => {
 | 
			
		||||
        return <Tab key={file.name} header={`${file.name}.wat`} />
 | 
			
		||||
      })}
 | 
			
		||||
    </Tabs>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const compiledSize = activeFile?.compiledContent?.byteLength || 0
 | 
			
		||||
  const color =
 | 
			
		||||
    compiledSize > FILESIZE_BREAKPOINTS[1]
 | 
			
		||||
      ? '$error'
 | 
			
		||||
      : compiledSize > FILESIZE_BREAKPOINTS[0]
 | 
			
		||||
      ? '$warning'
 | 
			
		||||
      : '$success'
 | 
			
		||||
 | 
			
		||||
  const isContentChanged = activeFile && activeFile.compiledValueSnapshot !== activeFile.content
 | 
			
		||||
  // const hasDeployErrors = activeFile && activeFile.containsErrors;
 | 
			
		||||
 | 
			
		||||
  const CompiledStatView = activeFile && (
 | 
			
		||||
    <Flex
 | 
			
		||||
      column
 | 
			
		||||
      align="center"
 | 
			
		||||
      css={{
 | 
			
		||||
        fontSize: '$sm',
 | 
			
		||||
        fontFamily: '$monospace',
 | 
			
		||||
        textAlign: 'center'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Flex row align="center">
 | 
			
		||||
        <Text css={{ mr: '$1' }}>Compiled {activeFile.name.split('.')[0] + '.wasm'}</Text>
 | 
			
		||||
        {activeFile?.lastCompiled && <ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />}
 | 
			
		||||
 | 
			
		||||
        {activeFile.compiledContent?.byteLength && (
 | 
			
		||||
          <Text css={{ ml: '$2', color }}>({filesize(activeFile.compiledContent.byteLength)})</Text>
 | 
			
		||||
        )}
 | 
			
		||||
      </Flex>
 | 
			
		||||
      {activeFile.compiledContent?.byteLength && activeFile.compiledContent?.byteLength >= 64000 && (
 | 
			
		||||
        <Flex css={{ flexDirection: 'column', py: '$3', pb: '$1' }}>
 | 
			
		||||
          <Text css={{ ml: '$2', color: '$error' }}>
 | 
			
		||||
            File size is larger than 64kB, cannot set hook!
 | 
			
		||||
          </Text>
 | 
			
		||||
        </Flex>
 | 
			
		||||
      )}
 | 
			
		||||
      <Button variant="link" onClick={() => setShowContent(true)}>
 | 
			
		||||
        View as WAT-file
 | 
			
		||||
      </Button>
 | 
			
		||||
      {isContentChanged && (
 | 
			
		||||
        <Text warning>
 | 
			
		||||
          File contents were changed after last compile, compile again to incorporate your latest
 | 
			
		||||
          changes in the build.
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
    </Flex>
 | 
			
		||||
  )
 | 
			
		||||
  const NoContentView = !snap.loading && router.isReady && (
 | 
			
		||||
    <Text
 | 
			
		||||
      css={{
 | 
			
		||||
        mt: '-60px',
 | 
			
		||||
        fontSize: '$sm',
 | 
			
		||||
        fontFamily: '$monospace',
 | 
			
		||||
        maxWidth: '300px',
 | 
			
		||||
        textAlign: 'center'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {`You haven't compiled any files yet, compile files on `}
 | 
			
		||||
      <NextLink shallow href={`/develop/${router.query.slug}`} passHref>
 | 
			
		||||
        <Link as="a">develop view</Link>
 | 
			
		||||
      </NextLink>
 | 
			
		||||
    </Text>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const isContent = snap.files?.filter(file => file.compiledWatContent).length > 0 && router.isReady
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      css={{
 | 
			
		||||
        flex: 1,
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        flexDirection: 'column',
 | 
			
		||||
        backgroundColor: '$mauve2',
 | 
			
		||||
        width: '100%'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <EditorNavigation renderNav={renderNav} />
 | 
			
		||||
      <Container
 | 
			
		||||
        css={{
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          flex: 1,
 | 
			
		||||
          justifyContent: 'center',
 | 
			
		||||
          alignItems: 'center'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {!isContent ? (
 | 
			
		||||
          NoContentView
 | 
			
		||||
        ) : !showContent ? (
 | 
			
		||||
          CompiledStatView
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Monaco
 | 
			
		||||
            className="hooks-editor"
 | 
			
		||||
            defaultLanguage={'wat'}
 | 
			
		||||
            language={'wat'}
 | 
			
		||||
            path={`file://tmp/c/${activeFile?.name}.wat`}
 | 
			
		||||
            value={activeFile?.compiledWatContent || ''}
 | 
			
		||||
            beforeMount={monaco => {
 | 
			
		||||
              monaco.languages.register({ id: 'wat' })
 | 
			
		||||
              monaco.languages.setLanguageConfiguration('wat', wat.config)
 | 
			
		||||
              monaco.languages.setMonarchTokensProvider('wat', wat.tokens)
 | 
			
		||||
            }}
 | 
			
		||||
            onMount={editor => {
 | 
			
		||||
              editor.updateOptions({
 | 
			
		||||
                glyphMargin: true,
 | 
			
		||||
                readOnly: true
 | 
			
		||||
              })
 | 
			
		||||
            }}
 | 
			
		||||
            theme={theme === 'dark' ? 'dark' : 'light'}
 | 
			
		||||
            overlay={
 | 
			
		||||
              <Flex
 | 
			
		||||
                css={{
 | 
			
		||||
                  m: '$1',
 | 
			
		||||
                  ml: 'auto',
 | 
			
		||||
                  fontSize: '$sm',
 | 
			
		||||
                  color: '$textMuted'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Link onClick={() => setShowContent(false)}>Exit editor mode</Link>
 | 
			
		||||
              </Flex>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </Container>
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default DeployEditor
 | 
			
		||||
@@ -1,103 +1,88 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import * as Stiches from "@stitches/react";
 | 
			
		||||
import { keyframes } from "@stitches/react";
 | 
			
		||||
import { violet, blackA, mauve, whiteA } from "@radix-ui/colors";
 | 
			
		||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import * as Stiches from '@stitches/react'
 | 
			
		||||
import { keyframes } from '@stitches/react'
 | 
			
		||||
import { blackA } from '@radix-ui/colors'
 | 
			
		||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const overlayShow = keyframes({
 | 
			
		||||
  "0%": { opacity: 0 },
 | 
			
		||||
  "100%": { opacity: 1 },
 | 
			
		||||
});
 | 
			
		||||
  '0%': { opacity: 0.01 },
 | 
			
		||||
  '100%': { opacity: 1 }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const contentShow = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translate(-50%, -48%) scale(.96)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translate(-50%, -50%) scale(1)" },
 | 
			
		||||
});
 | 
			
		||||
  '0%': { opacity: 0.01 },
 | 
			
		||||
  '100%': { opacity: 1 }
 | 
			
		||||
})
 | 
			
		||||
const StyledOverlay = styled(DialogPrimitive.Overlay, {
 | 
			
		||||
  zIndex: 1000,
 | 
			
		||||
  zIndex: 3000,
 | 
			
		||||
  backgroundColor: blackA.blackA9,
 | 
			
		||||
  position: "fixed",
 | 
			
		||||
  position: 'fixed',
 | 
			
		||||
  inset: 0,
 | 
			
		||||
  "@media (prefers-reduced-motion: no-preference)": {
 | 
			
		||||
    animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
 | 
			
		||||
  top: 0,
 | 
			
		||||
  left: 0,
 | 
			
		||||
  right: 0,
 | 
			
		||||
  bottom: 0,
 | 
			
		||||
  display: 'grid',
 | 
			
		||||
  placeItems: 'center',
 | 
			
		||||
  overflowY: 'auto',
 | 
			
		||||
  '@media (prefers-reduced-motion: no-preference)': {
 | 
			
		||||
    animation: `${overlayShow} 250ms cubic-bezier(0.16, 1, 0.3, 1)`
 | 
			
		||||
  },
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    backgroundColor: blackA.blackA9,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    backgroundColor: blackA.blackA11
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledContent = styled(DialogPrimitive.Content, {
 | 
			
		||||
  zIndex: 1000,
 | 
			
		||||
  backgroundColor: "$slate2",
 | 
			
		||||
  color: "$slate12",
 | 
			
		||||
  borderRadius: "$md",
 | 
			
		||||
  boxShadow:
 | 
			
		||||
    "0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
 | 
			
		||||
  position: "fixed",
 | 
			
		||||
  top: "50%",
 | 
			
		||||
  left: "50%",
 | 
			
		||||
  transform: "translate(-50%, -50%)",
 | 
			
		||||
  width: "90vw",
 | 
			
		||||
  maxWidth: "450px",
 | 
			
		||||
  maxHeight: "85vh",
 | 
			
		||||
  backgroundColor: '$mauve2',
 | 
			
		||||
  color: '$mauve12',
 | 
			
		||||
  borderRadius: '$md',
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  mb: '15%',
 | 
			
		||||
  boxShadow: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)',
 | 
			
		||||
  width: '90vw',
 | 
			
		||||
  maxWidth: '450px',
 | 
			
		||||
  // maxHeight: "85vh",
 | 
			
		||||
  padding: 25,
 | 
			
		||||
  "@media (prefers-reduced-motion: no-preference)": {
 | 
			
		||||
    animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
 | 
			
		||||
  '@media (prefers-reduced-motion: no-preference)': {
 | 
			
		||||
    animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
 | 
			
		||||
  },
 | 
			
		||||
  "&:focus": { outline: "none" },
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    backgroundColor: "$slate5",
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      "0px 10px 38px 0px rgba(22, 23, 24, 0.85), 0px 10px 20px 0px rgba(22, 23, 24, 0.6)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
  '&:focus': { outline: 'none' },
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    backgroundColor: '$mauve5',
 | 
			
		||||
    boxShadow: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const Content: React.FC<{ css?: Stiches.CSS }> = ({ css, children }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <StyledOverlay />
 | 
			
		||||
    <StyledOverlay>
 | 
			
		||||
      <StyledContent css={css}>{children}</StyledContent>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
    </StyledOverlay>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledTitle = styled(DialogPrimitive.Title, {
 | 
			
		||||
  margin: 0,
 | 
			
		||||
  fontWeight: 500,
 | 
			
		||||
  color: "$slate12",
 | 
			
		||||
  fontSize: 17,
 | 
			
		||||
});
 | 
			
		||||
  color: '$mauve12',
 | 
			
		||||
  fontSize: 17
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledDescription = styled(DialogPrimitive.Description, {
 | 
			
		||||
  margin: "10px 0 20px",
 | 
			
		||||
  color: "$slate11",
 | 
			
		||||
  margin: '10px 0 10px',
 | 
			
		||||
  color: '$mauve11',
 | 
			
		||||
  fontSize: 15,
 | 
			
		||||
  lineHeight: 1.5,
 | 
			
		||||
});
 | 
			
		||||
  lineHeight: 1.5
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
export const Dialog = DialogPrimitive.Root;
 | 
			
		||||
export const DialogTrigger = DialogPrimitive.Trigger;
 | 
			
		||||
export const DialogContent = Content;
 | 
			
		||||
export const DialogTitle = StyledTitle;
 | 
			
		||||
export const DialogDescription = StyledDescription;
 | 
			
		||||
export const DialogClose = DialogPrimitive.Close;
 | 
			
		||||
 | 
			
		||||
const Input = styled("input", {
 | 
			
		||||
  all: "unset",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  flex: "1",
 | 
			
		||||
  display: "inline-flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  borderRadius: 4,
 | 
			
		||||
  padding: "0 10px",
 | 
			
		||||
  fontSize: 15,
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: violet.violet11,
 | 
			
		||||
  boxShadow: `0 0 0 1px ${violet.violet7}`,
 | 
			
		||||
  height: 35,
 | 
			
		||||
 | 
			
		||||
  "&:focus": { boxShadow: `0 0 0 2px ${violet.violet8}` },
 | 
			
		||||
});
 | 
			
		||||
export const Dialog = styled(DialogPrimitive.Root)
 | 
			
		||||
export const DialogTrigger = DialogPrimitive.Trigger
 | 
			
		||||
export const DialogContent = Content
 | 
			
		||||
export const DialogTitle = StyledTitle
 | 
			
		||||
export const DialogDescription = StyledDescription
 | 
			
		||||
export const DialogClose = DialogPrimitive.Close
 | 
			
		||||
export const DialogPortal = DialogPrimitive.Portal
 | 
			
		||||
 
 | 
			
		||||
@@ -1,131 +1,120 @@
 | 
			
		||||
import { keyframes } from "@stitches/react";
 | 
			
		||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
 | 
			
		||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
 | 
			
		||||
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import { blackA, slateDark } from "@radix-ui/colors";
 | 
			
		||||
 | 
			
		||||
const slideUpAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateY(2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideRightAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateX(-2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideDownAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateY(-2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slideLeftAndFade = keyframes({
 | 
			
		||||
  "0%": { opacity: 0, transform: "translateX(2px)" },
 | 
			
		||||
  "100%": { opacity: 1, transform: "translateX(0)" },
 | 
			
		||||
});
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import {
 | 
			
		||||
  slideDownAndFade,
 | 
			
		||||
  slideLeftAndFade,
 | 
			
		||||
  slideRightAndFade,
 | 
			
		||||
  slideUpAndFade
 | 
			
		||||
} from '../styles/keyframes'
 | 
			
		||||
 | 
			
		||||
const StyledContent = styled(DropdownMenuPrimitive.Content, {
 | 
			
		||||
  minWidth: 220,
 | 
			
		||||
  backgroundColor: "$slate2",
 | 
			
		||||
  backgroundColor: '$mauve2',
 | 
			
		||||
  borderRadius: 6,
 | 
			
		||||
  padding: 5,
 | 
			
		||||
  boxShadow:
 | 
			
		||||
    "0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
 | 
			
		||||
  "@media (prefers-reduced-motion: no-preference)": {
 | 
			
		||||
    animationDuration: "400ms",
 | 
			
		||||
    animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
 | 
			
		||||
    willChange: "transform, opacity",
 | 
			
		||||
    '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
 | 
			
		||||
  '@media (prefers-reduced-motion: no-preference)': {
 | 
			
		||||
    animationDuration: '400ms',
 | 
			
		||||
    animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
 | 
			
		||||
    willChange: 'transform, opacity',
 | 
			
		||||
    '&[data-state="open"]': {
 | 
			
		||||
      '&[data-side="top"]': { animationName: slideDownAndFade },
 | 
			
		||||
      '&[data-side="right"]': { animationName: slideLeftAndFade },
 | 
			
		||||
      '&[data-side="bottom"]': { animationName: slideUpAndFade },
 | 
			
		||||
      '&[data-side="left"]': { animationName: slideRightAndFade },
 | 
			
		||||
    },
 | 
			
		||||
      '&[data-side="left"]': { animationName: slideRightAndFade }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    backgroundColor: "$slate5",
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    backgroundColor: '$mauve5',
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      "0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
      '0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const itemStyles = {
 | 
			
		||||
  all: "unset",
 | 
			
		||||
  all: 'unset',
 | 
			
		||||
  fontSize: 13,
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: "$slate12",
 | 
			
		||||
  color: '$mauve12',
 | 
			
		||||
  borderRadius: 3,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  height: 32,
 | 
			
		||||
  padding: "0 5px",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  paddingLeft: "5px",
 | 
			
		||||
  userSelect: "none",
 | 
			
		||||
  gap: "$2",
 | 
			
		||||
  padding: '0 5px',
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  paddingLeft: '5px',
 | 
			
		||||
  userSelect: 'none',
 | 
			
		||||
  py: '$0.5',
 | 
			
		||||
  pr: '$2',
 | 
			
		||||
  gap: '$2',
 | 
			
		||||
 | 
			
		||||
  "&[data-disabled]": {
 | 
			
		||||
    color: "$slate9",
 | 
			
		||||
    pointerEvents: "none",
 | 
			
		||||
  '&[data-disabled]': {
 | 
			
		||||
    color: '$mauve9',
 | 
			
		||||
    pointerEvents: 'none'
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "&:focus": {
 | 
			
		||||
    backgroundColor: "$pink9",
 | 
			
		||||
    color: "$white",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
  '&:focus': {
 | 
			
		||||
    backgroundColor: '$purple9',
 | 
			
		||||
    color: '$white'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledItem = styled(DropdownMenuPrimitive.Item, { ...itemStyles });
 | 
			
		||||
const StyledItem = styled(DropdownMenuPrimitive.Item, { ...itemStyles })
 | 
			
		||||
const StyledCheckboxItem = styled(DropdownMenuPrimitive.CheckboxItem, {
 | 
			
		||||
  ...itemStyles,
 | 
			
		||||
});
 | 
			
		||||
  ...itemStyles
 | 
			
		||||
})
 | 
			
		||||
const StyledRadioItem = styled(DropdownMenuPrimitive.RadioItem, {
 | 
			
		||||
  ...itemStyles,
 | 
			
		||||
});
 | 
			
		||||
  ...itemStyles
 | 
			
		||||
})
 | 
			
		||||
const StyledTriggerItem = styled(DropdownMenuPrimitive.TriggerItem, {
 | 
			
		||||
  '&[data-state="open"]': {
 | 
			
		||||
    backgroundColor: "$pink9",
 | 
			
		||||
    color: "$pink9",
 | 
			
		||||
    backgroundColor: '$purple9',
 | 
			
		||||
    color: '$purple9'
 | 
			
		||||
  },
 | 
			
		||||
  ...itemStyles,
 | 
			
		||||
});
 | 
			
		||||
  ...itemStyles
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledLabel = styled(DropdownMenuPrimitive.Label, {
 | 
			
		||||
  paddingLeft: 25,
 | 
			
		||||
  fontSize: 12,
 | 
			
		||||
  lineHeight: "25px",
 | 
			
		||||
  color: "$slate11",
 | 
			
		||||
});
 | 
			
		||||
  lineHeight: '25px',
 | 
			
		||||
  color: '$mauve11'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledSeparator = styled(DropdownMenuPrimitive.Separator, {
 | 
			
		||||
  height: 1,
 | 
			
		||||
  backgroundColor: "$slate7",
 | 
			
		||||
  margin: 5,
 | 
			
		||||
});
 | 
			
		||||
  backgroundColor: '$mauve7',
 | 
			
		||||
  margin: 5
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledItemIndicator = styled(DropdownMenuPrimitive.ItemIndicator, {
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  position: 'absolute',
 | 
			
		||||
  left: 0,
 | 
			
		||||
  width: 25,
 | 
			
		||||
  display: "inline-flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
});
 | 
			
		||||
  display: 'inline-flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'center'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledArrow = styled(DropdownMenuPrimitive.Arrow, {
 | 
			
		||||
  fill: "$slate2",
 | 
			
		||||
});
 | 
			
		||||
  fill: '$mauve2',
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    fill: '$mauve5'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
export const DropdownMenu = DropdownMenuPrimitive.Root;
 | 
			
		||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
 | 
			
		||||
export const DropdownMenuContent = StyledContent;
 | 
			
		||||
export const DropdownMenuItem = StyledItem;
 | 
			
		||||
export const DropdownMenuCheckboxItem = StyledCheckboxItem;
 | 
			
		||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
 | 
			
		||||
export const DropdownMenuRadioItem = StyledRadioItem;
 | 
			
		||||
export const DropdownMenuItemIndicator = StyledItemIndicator;
 | 
			
		||||
export const DropdownMenuTriggerItem = StyledTriggerItem;
 | 
			
		||||
export const DropdownMenuLabel = StyledLabel;
 | 
			
		||||
export const DropdownMenuSeparator = StyledSeparator;
 | 
			
		||||
export const DropdownMenuArrow = StyledArrow;
 | 
			
		||||
export const DropdownMenu = DropdownMenuPrimitive.Root
 | 
			
		||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
 | 
			
		||||
export const DropdownMenuContent = StyledContent
 | 
			
		||||
export const DropdownMenuItem = StyledItem
 | 
			
		||||
export const DropdownMenuCheckboxItem = StyledCheckboxItem
 | 
			
		||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
 | 
			
		||||
export const DropdownMenuRadioItem = StyledRadioItem
 | 
			
		||||
export const DropdownMenuItemIndicator = StyledItemIndicator
 | 
			
		||||
export const DropdownMenuTriggerItem = StyledTriggerItem
 | 
			
		||||
export const DropdownMenuLabel = StyledLabel
 | 
			
		||||
export const DropdownMenuSeparator = StyledSeparator
 | 
			
		||||
export const DropdownMenuArrow = StyledArrow
 | 
			
		||||
 
 | 
			
		||||
@@ -1,234 +1,379 @@
 | 
			
		||||
import React, { useRef, useState } from "react";
 | 
			
		||||
import { Plus, Share, DownloadSimple, Gear, X } from "phosphor-react";
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import React, { useState, useEffect, useRef, ReactNode } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Share,
 | 
			
		||||
  DownloadSimple,
 | 
			
		||||
  Gear,
 | 
			
		||||
  X,
 | 
			
		||||
  GithubLogo,
 | 
			
		||||
  SignOut,
 | 
			
		||||
  ArrowSquareOut,
 | 
			
		||||
  CloudArrowUp,
 | 
			
		||||
  CaretDown,
 | 
			
		||||
  User,
 | 
			
		||||
  FilePlus
 | 
			
		||||
} from 'phosphor-react'
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import {
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
  DropdownMenuTrigger,
 | 
			
		||||
  DropdownMenuContent,
 | 
			
		||||
  DropdownMenuItem,
 | 
			
		||||
  DropdownMenuArrow,
 | 
			
		||||
  DropdownMenuSeparator
 | 
			
		||||
} from './DropdownMenu'
 | 
			
		||||
import NewWindow from 'react-new-window'
 | 
			
		||||
import { signOut, useSession } from 'next-auth/react'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
 | 
			
		||||
import { createNewFile, state, updateEditorSettings } from "../state";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import { syncToGist, updateEditorSettings, downloadAsZip } from '../state/actions'
 | 
			
		||||
import state from '../state'
 | 
			
		||||
import Box from './Box'
 | 
			
		||||
import Button from './Button'
 | 
			
		||||
import Container from './Container'
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogClose,
 | 
			
		||||
} from "./Dialog";
 | 
			
		||||
import Flex from "./Flex";
 | 
			
		||||
import Stack from "./Stack";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import { useSession } from "next-auth/react";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
  DialogClose
 | 
			
		||||
} from './Dialog'
 | 
			
		||||
import Flex from './Flex'
 | 
			
		||||
import Stack from './Stack'
 | 
			
		||||
import { Input, Label } from './Input'
 | 
			
		||||
import Tooltip from './Tooltip'
 | 
			
		||||
import { showAlert } from '../state/actions/showAlert'
 | 
			
		||||
 | 
			
		||||
const EditorNavigation = () => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const [filename, setFilename] = useState("");
 | 
			
		||||
  const { theme } = useTheme();
 | 
			
		||||
  const { data: session, status } = useSession();
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
 | 
			
		||||
const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  const [editorSettingsOpen, setEditorSettingsOpen] = useState(false)
 | 
			
		||||
  const { data: session, status } = useSession()
 | 
			
		||||
  const [popup, setPopUp] = useState(false)
 | 
			
		||||
  const [editorSettings, setEditorSettings] = useState(snap.editorSettings)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (session && session.user && popup) {
 | 
			
		||||
      setPopUp(false)
 | 
			
		||||
    }
 | 
			
		||||
  }, [session, popup])
 | 
			
		||||
 | 
			
		||||
  const showNewGistAlert = () => {
 | 
			
		||||
    showAlert('Are you sure?', {
 | 
			
		||||
      body: (
 | 
			
		||||
        <>
 | 
			
		||||
          This action will create new <strong>public</strong> Github Gist from your current saved
 | 
			
		||||
          files. You can delete gist anytime from your GitHub Gists page.
 | 
			
		||||
        </>
 | 
			
		||||
      ),
 | 
			
		||||
      cancelText: 'Cancel',
 | 
			
		||||
      confirmText: 'Create new Gist',
 | 
			
		||||
      confirmPrefix: <FilePlus size="15px" />,
 | 
			
		||||
      onConfirm: () => syncToGist(session, true)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const scrollRef = useRef<HTMLDivElement>(null)
 | 
			
		||||
  const containerRef = useRef<HTMLDivElement>(null)
 | 
			
		||||
  return (
 | 
			
		||||
    <Flex css={{ flexShrink: 0, gap: "$0" }}>
 | 
			
		||||
      <Flex css={{ overflowX: "scroll", py: "$3", flex: 1 }}>
 | 
			
		||||
        <Container css={{ flex: 1 }}>
 | 
			
		||||
          <Stack css={{ gap: "$3", flex: 1, flexWrap: "nowrap" }}>
 | 
			
		||||
            {state.loading && "loading"}
 | 
			
		||||
            {snap.files &&
 | 
			
		||||
              snap.files.length > 0 &&
 | 
			
		||||
              snap.files?.map((file, index) => (
 | 
			
		||||
                <Button
 | 
			
		||||
                  size="sm"
 | 
			
		||||
                  outline={snap.active !== index}
 | 
			
		||||
                  onClick={() => (state.active = index)}
 | 
			
		||||
                  key={file.name}
 | 
			
		||||
                  css={{
 | 
			
		||||
                    "&:hover": {
 | 
			
		||||
                      span: {
 | 
			
		||||
                        visibility: "visible",
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {file.name}
 | 
			
		||||
                  <Box
 | 
			
		||||
                    as="span"
 | 
			
		||||
                    css={{
 | 
			
		||||
                      display: "flex",
 | 
			
		||||
                      p: "1px",
 | 
			
		||||
                      borderRadius: "$full",
 | 
			
		||||
                      mr: "-4px",
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={() => state.files.splice(index, 1)}
 | 
			
		||||
                  >
 | 
			
		||||
                    <X size="13px" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </Button>
 | 
			
		||||
              ))}
 | 
			
		||||
 | 
			
		||||
            <Dialog>
 | 
			
		||||
              <DialogTrigger asChild>
 | 
			
		||||
                <Button
 | 
			
		||||
                  ghost
 | 
			
		||||
                  size="sm"
 | 
			
		||||
                  css={{ alignItems: "center", px: "$2", mr: "$3" }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Plus size="16px" />{" "}
 | 
			
		||||
                  {snap.files.length === 0 && "Add new file"}
 | 
			
		||||
                </Button>
 | 
			
		||||
              </DialogTrigger>
 | 
			
		||||
              <DialogContent>
 | 
			
		||||
                <DialogTitle>Create new file</DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                  <span>
 | 
			
		||||
                    Create empty C file or select one of the existing ones
 | 
			
		||||
                  </span>
 | 
			
		||||
                  <input
 | 
			
		||||
                    value={filename}
 | 
			
		||||
                    onChange={(e) => setFilename(e.target.value)}
 | 
			
		||||
                  />
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
 | 
			
		||||
                <Flex
 | 
			
		||||
                  css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}
 | 
			
		||||
                >
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Button outline>Cancel</Button>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Button
 | 
			
		||||
                      variant="primary"
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        createNewFile(filename);
 | 
			
		||||
                        // reset
 | 
			
		||||
                        setFilename("");
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      Create file
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                <DialogClose asChild>
 | 
			
		||||
                  <Box css={{ position: "absolute", top: "$3", right: "$3" }}>
 | 
			
		||||
                    <X size="20px" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </DialogClose>
 | 
			
		||||
              </DialogContent>
 | 
			
		||||
            </Dialog>
 | 
			
		||||
          </Stack>
 | 
			
		||||
    <Flex css={{ flexShrink: 0, gap: '$0' }}>
 | 
			
		||||
      <Flex
 | 
			
		||||
        id="kissa"
 | 
			
		||||
        ref={scrollRef}
 | 
			
		||||
        css={{
 | 
			
		||||
          overflowX: 'scroll',
 | 
			
		||||
          overflowY: 'hidden',
 | 
			
		||||
          py: '$3',
 | 
			
		||||
          pb: '$0',
 | 
			
		||||
          flex: 1,
 | 
			
		||||
          '&::-webkit-scrollbar': {
 | 
			
		||||
            height: '0.3em',
 | 
			
		||||
            background: 'rgba(0,0,0,.0)'
 | 
			
		||||
          },
 | 
			
		||||
          '&::-webkit-scrollbar-gutter': 'stable',
 | 
			
		||||
          '&::-webkit-scrollbar-thumb': {
 | 
			
		||||
            backgroundColor: 'rgba(0,0,0,.2)',
 | 
			
		||||
            outline: '0px',
 | 
			
		||||
            borderRadius: '9999px'
 | 
			
		||||
          },
 | 
			
		||||
          scrollbarColor: 'rgba(0,0,0,.2) rgba(0,0,0,0)',
 | 
			
		||||
          scrollbarGutter: 'stable',
 | 
			
		||||
          scrollbarWidth: 'thin',
 | 
			
		||||
          '.dark &': {
 | 
			
		||||
            '&::-webkit-scrollbar': {
 | 
			
		||||
              background: 'rgba(0,0,0,.0)'
 | 
			
		||||
            },
 | 
			
		||||
            '&::-webkit-scrollbar-gutter': 'stable',
 | 
			
		||||
            '&::-webkit-scrollbar-thumb': {
 | 
			
		||||
              backgroundColor: 'rgba(255,255,255,.2)',
 | 
			
		||||
              outline: '0px',
 | 
			
		||||
              borderRadius: '9999px'
 | 
			
		||||
            },
 | 
			
		||||
            scrollbarColor: 'rgba(255,255,255,.2) rgba(0,0,0,0)',
 | 
			
		||||
            scrollbarGutter: 'stable',
 | 
			
		||||
            scrollbarWidth: 'thin'
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        onWheelCapture={e => {
 | 
			
		||||
          if (scrollRef.current) {
 | 
			
		||||
            scrollRef.current.scrollLeft += e.deltaY
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Container css={{ flex: 1 }} ref={containerRef}>
 | 
			
		||||
          {renderNav?.()}
 | 
			
		||||
        </Container>
 | 
			
		||||
      </Flex>
 | 
			
		||||
      <Flex
 | 
			
		||||
        css={{
 | 
			
		||||
          py: "$3",
 | 
			
		||||
          backgroundColor: "$slate3",
 | 
			
		||||
          zIndex: 1,
 | 
			
		||||
          py: '$3',
 | 
			
		||||
          backgroundColor: '$mauve2',
 | 
			
		||||
          zIndex: 1
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Container css={{ width: "unset" }}>
 | 
			
		||||
        <Container css={{ width: 'unset', display: 'flex', alignItems: 'center' }}>
 | 
			
		||||
          {status === 'authenticated' ? (
 | 
			
		||||
            <DropdownMenu>
 | 
			
		||||
              <DropdownMenuTrigger asChild>
 | 
			
		||||
                <Box
 | 
			
		||||
                  css={{
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    borderRadius: '$full',
 | 
			
		||||
                    overflow: 'hidden',
 | 
			
		||||
                    width: '$6',
 | 
			
		||||
                    height: '$6',
 | 
			
		||||
                    boxShadow: '0px 0px 0px 1px $colors$mauve11',
 | 
			
		||||
                    position: 'relative',
 | 
			
		||||
                    mr: '$3',
 | 
			
		||||
                    '@hover': {
 | 
			
		||||
                      '&:hover': {
 | 
			
		||||
                        cursor: 'pointer',
 | 
			
		||||
                        boxShadow: '0px 0px 0px 1px $colors$mauve12'
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Image
 | 
			
		||||
                    src={session?.user?.image || ''}
 | 
			
		||||
                    width="30px"
 | 
			
		||||
                    height="30px"
 | 
			
		||||
                    objectFit="cover"
 | 
			
		||||
                    alt="User avatar"
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
              </DropdownMenuTrigger>
 | 
			
		||||
              <DropdownMenuContent>
 | 
			
		||||
                <DropdownMenuItem disabled onClick={() => signOut()}>
 | 
			
		||||
                  <User size="16px" /> {session?.user?.username} ({session?.user.name})
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
                <DropdownMenuItem
 | 
			
		||||
                  onClick={() => window.open(`http://gist.github.com/${session?.user.username}`)}
 | 
			
		||||
                >
 | 
			
		||||
                  <ArrowSquareOut size="16px" />
 | 
			
		||||
                  Go to your Gist
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
                <DropdownMenuSeparator />
 | 
			
		||||
                <DropdownMenuItem onClick={() => signOut({ callbackUrl: '/' })}>
 | 
			
		||||
                  <SignOut size="16px" /> Log out
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
 | 
			
		||||
                <DropdownMenuArrow offset={10} />
 | 
			
		||||
              </DropdownMenuContent>
 | 
			
		||||
            </DropdownMenu>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Button outline size="sm" css={{ mr: '$3' }} onClick={() => setPopUp(true)}>
 | 
			
		||||
              <GithubLogo size="16px" /> Login
 | 
			
		||||
            </Button>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <Stack
 | 
			
		||||
            css={{
 | 
			
		||||
              display: "inline-flex",
 | 
			
		||||
              marginLeft: "auto",
 | 
			
		||||
              display: 'inline-flex',
 | 
			
		||||
              marginLeft: 'auto',
 | 
			
		||||
              flexShrink: 0,
 | 
			
		||||
              gap: "$0",
 | 
			
		||||
              border: "1px solid $slate10",
 | 
			
		||||
              borderRadius: "$sm",
 | 
			
		||||
              gap: '$0',
 | 
			
		||||
              borderRadius: '$sm',
 | 
			
		||||
              boxShadow: 'inset 0px 0px 0px 1px $colors$mauve10',
 | 
			
		||||
              zIndex: 9,
 | 
			
		||||
              position: "relative",
 | 
			
		||||
              overflow: "hidden",
 | 
			
		||||
              position: 'relative',
 | 
			
		||||
              button: {
 | 
			
		||||
                borderRadius: "$0",
 | 
			
		||||
                px: "$2",
 | 
			
		||||
                alignSelf: "flex-start",
 | 
			
		||||
                borderRadius: 0,
 | 
			
		||||
                px: '$2',
 | 
			
		||||
                alignSelf: 'flex-start',
 | 
			
		||||
                boxShadow: 'none'
 | 
			
		||||
              },
 | 
			
		||||
              'button:not(:first-child):not(:last-child)': {
 | 
			
		||||
                borderRight: 0,
 | 
			
		||||
                borderLeft: 0
 | 
			
		||||
              },
 | 
			
		||||
              'button:first-child': {
 | 
			
		||||
                borderTopLeftRadius: '$sm',
 | 
			
		||||
                borderBottomLeftRadius: '$sm'
 | 
			
		||||
              },
 | 
			
		||||
              'button:last-child': {
 | 
			
		||||
                borderTopRightRadius: '$sm',
 | 
			
		||||
                borderBottomRightRadius: '$sm',
 | 
			
		||||
                boxShadow: 'inset 0px 0px 0px 1px $colors$mauve10',
 | 
			
		||||
                '&:hover': {
 | 
			
		||||
                  boxShadow: 'inset 0px 0px 0px 1px $colors$mauve12'
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Button ghost size="sm" css={{ alignItems: "center" }}>
 | 
			
		||||
              <DownloadSimple size="16px" />
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Dialog>
 | 
			
		||||
              <DialogTrigger asChild>
 | 
			
		||||
                <Button ghost size="sm" css={{ alignItems: "center" }}>
 | 
			
		||||
            <Tooltip content="Download as ZIP">
 | 
			
		||||
              <Button
 | 
			
		||||
                isLoading={snap.zipLoading}
 | 
			
		||||
                onClick={downloadAsZip}
 | 
			
		||||
                outline
 | 
			
		||||
                size="sm"
 | 
			
		||||
                css={{ alignItems: 'center' }}
 | 
			
		||||
              >
 | 
			
		||||
                <DownloadSimple size="16px" />
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
            <Tooltip content="Copy share link to clipboard">
 | 
			
		||||
              <Button
 | 
			
		||||
                outline
 | 
			
		||||
                size="sm"
 | 
			
		||||
                css={{ alignItems: 'center' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  navigator.clipboard.writeText(`${window.location.origin}/develop/${snap.gistId}`)
 | 
			
		||||
                  toast.success('Copied share link to clipboard!')
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Share size="16px" />
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
            <Tooltip
 | 
			
		||||
              content={
 | 
			
		||||
                session && session.user
 | 
			
		||||
                  ? snap.gistOwner === session?.user.username
 | 
			
		||||
                    ? 'Sync to Gist'
 | 
			
		||||
                    : 'Create as a new Gist'
 | 
			
		||||
                  : 'You need to be logged in to sync with Gist'
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              <Button
 | 
			
		||||
                outline
 | 
			
		||||
                size="sm"
 | 
			
		||||
                isDisabled={!session || !session.user}
 | 
			
		||||
                isLoading={snap.gistLoading}
 | 
			
		||||
                css={{ alignItems: 'center' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  if (!session || !session.user) {
 | 
			
		||||
                    return
 | 
			
		||||
                  }
 | 
			
		||||
                  if (snap.gistOwner === session?.user.username) {
 | 
			
		||||
                    syncToGist(session)
 | 
			
		||||
                  } else {
 | 
			
		||||
                    showNewGistAlert()
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {snap.gistOwner === session?.user.username ? (
 | 
			
		||||
                  <CloudArrowUp size="16px" />
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <FilePlus size="16px" />
 | 
			
		||||
                )}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
 | 
			
		||||
            <DropdownMenu>
 | 
			
		||||
              <DropdownMenuTrigger asChild>
 | 
			
		||||
                <Button outline size="sm">
 | 
			
		||||
                  <CaretDown size="16px" />
 | 
			
		||||
                </Button>
 | 
			
		||||
              </DropdownMenuTrigger>
 | 
			
		||||
              <DropdownMenuContent>
 | 
			
		||||
                <DropdownMenuItem disabled={snap.zipLoading} onClick={downloadAsZip}>
 | 
			
		||||
                  <DownloadSimple size="16px" /> Download as ZIP
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
                <DropdownMenuItem
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    navigator.clipboard.writeText(
 | 
			
		||||
                      `${window.location.origin}/develop/${snap.gistId}`
 | 
			
		||||
                    )
 | 
			
		||||
                    toast.success('Copied share link to clipboard!')
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Share size="16px" />
 | 
			
		||||
                </Button>
 | 
			
		||||
              </DialogTrigger>
 | 
			
		||||
              <DialogContent>
 | 
			
		||||
                <DialogTitle>Share hook</DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                  <span>
 | 
			
		||||
                    We will store your hook code in public GitHub Gist and
 | 
			
		||||
                    generate link to that
 | 
			
		||||
                  </span>
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
 | 
			
		||||
                <Flex
 | 
			
		||||
                  css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}
 | 
			
		||||
                  Copy share link to clipboard
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
                <DropdownMenuItem
 | 
			
		||||
                  disabled={session?.user.username !== snap.gistOwner || !snap.gistId}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    syncToGist(session)
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Button outline>Cancel</Button>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                <DialogClose asChild>
 | 
			
		||||
                  <Box css={{ position: "absolute", top: "$3", right: "$3" }}>
 | 
			
		||||
                    <X size="20px" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </DialogClose>
 | 
			
		||||
              </DialogContent>
 | 
			
		||||
            </Dialog>
 | 
			
		||||
 | 
			
		||||
            <Dialog>
 | 
			
		||||
              <DialogTrigger asChild>
 | 
			
		||||
                <Button ghost size="sm" css={{ alignItems: "center" }}>
 | 
			
		||||
                  <Gear size="16px" />
 | 
			
		||||
                </Button>
 | 
			
		||||
              </DialogTrigger>
 | 
			
		||||
              <DialogContent>
 | 
			
		||||
                <DialogTitle>Editor settings</DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                  <span>You can edit your editor settings here</span>
 | 
			
		||||
                  <input
 | 
			
		||||
                    value={editorSettings.tabSize}
 | 
			
		||||
                    onChange={(e) =>
 | 
			
		||||
                      setEditorSettings((curr) => ({
 | 
			
		||||
                        ...curr,
 | 
			
		||||
                        tabSize: Number(e.target.value),
 | 
			
		||||
                      }))
 | 
			
		||||
                    }
 | 
			
		||||
                  />
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
 | 
			
		||||
                <Flex
 | 
			
		||||
                  css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}
 | 
			
		||||
                  <CloudArrowUp size="16px" /> Push to Gist
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
                <DropdownMenuSeparator />
 | 
			
		||||
                <DropdownMenuItem
 | 
			
		||||
                  disabled={status !== 'authenticated'}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    showNewGistAlert()
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Button
 | 
			
		||||
                      outline
 | 
			
		||||
                      onClick={() => updateEditorSettings(editorSettings)}
 | 
			
		||||
                    >
 | 
			
		||||
                      Cancel
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Button
 | 
			
		||||
                      variant="primary"
 | 
			
		||||
                      onClick={() => updateEditorSettings(editorSettings)}
 | 
			
		||||
                    >
 | 
			
		||||
                      Save changes
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                <DialogClose asChild>
 | 
			
		||||
                  <Box css={{ position: "absolute", top: "$3", right: "$3" }}>
 | 
			
		||||
                    <X size="20px" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </DialogClose>
 | 
			
		||||
              </DialogContent>
 | 
			
		||||
            </Dialog>
 | 
			
		||||
                  <FilePlus size="16px" /> Create as a new Gist
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
 | 
			
		||||
                <DropdownMenuItem onClick={() => setEditorSettingsOpen(true)}>
 | 
			
		||||
                  <Gear size="16px" /> Editor Settings
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
 | 
			
		||||
                <DropdownMenuArrow offset={10} />
 | 
			
		||||
              </DropdownMenuContent>
 | 
			
		||||
            </DropdownMenu>
 | 
			
		||||
          </Stack>
 | 
			
		||||
 | 
			
		||||
          {popup && !session ? <NewWindow center="parent" url="/sign-in" /> : null}
 | 
			
		||||
        </Container>
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default EditorNavigation;
 | 
			
		||||
      <Dialog open={editorSettingsOpen} onOpenChange={setEditorSettingsOpen}>
 | 
			
		||||
        <DialogTrigger asChild>
 | 
			
		||||
          {/* <Button outline size="sm" css={{ alignItems: "center" }}>
 | 
			
		||||
                  <Gear size="16px" />
 | 
			
		||||
                </Button> */}
 | 
			
		||||
        </DialogTrigger>
 | 
			
		||||
        <DialogContent>
 | 
			
		||||
          <DialogTitle>Editor settings</DialogTitle>
 | 
			
		||||
          <DialogDescription>
 | 
			
		||||
            <Label>Tab size</Label>
 | 
			
		||||
            <Input
 | 
			
		||||
              type="number"
 | 
			
		||||
              min="1"
 | 
			
		||||
              value={editorSettings.tabSize}
 | 
			
		||||
              onChange={e =>
 | 
			
		||||
                setEditorSettings(curr => ({
 | 
			
		||||
                  ...curr,
 | 
			
		||||
                  tabSize: Number(e.target.value)
 | 
			
		||||
                }))
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
          </DialogDescription>
 | 
			
		||||
 | 
			
		||||
          <Flex css={{ marginTop: 25, justifyContent: 'flex-end', gap: '$3' }}>
 | 
			
		||||
            <DialogClose asChild>
 | 
			
		||||
              <Button outline onClick={() => updateEditorSettings(editorSettings)}>
 | 
			
		||||
                Cancel
 | 
			
		||||
              </Button>
 | 
			
		||||
            </DialogClose>
 | 
			
		||||
            <DialogClose asChild>
 | 
			
		||||
              <Button variant="primary" onClick={() => updateEditorSettings(editorSettings)}>
 | 
			
		||||
                Save changes
 | 
			
		||||
              </Button>
 | 
			
		||||
            </DialogClose>
 | 
			
		||||
          </Flex>
 | 
			
		||||
          <DialogClose asChild>
 | 
			
		||||
            <Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
 | 
			
		||||
              <X size="20px" />
 | 
			
		||||
            </Box>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default EditorNavigation
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								components/EnrichLog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,73 @@
 | 
			
		||||
import { FC, useState } from 'react'
 | 
			
		||||
import regexifyString from 'regexify-string'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import { Link } from '.'
 | 
			
		||||
import state from '../state'
 | 
			
		||||
import { AccountDialog } from './Accounts'
 | 
			
		||||
import Tooltip from './Tooltip'
 | 
			
		||||
import hookSetCodes from '../content/hook-set-codes.json'
 | 
			
		||||
import { capitalize } from '../utils/helpers'
 | 
			
		||||
 | 
			
		||||
interface EnrichLogProps {
 | 
			
		||||
  str?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const EnrichLog: FC<EnrichLogProps> = ({ str }) => {
 | 
			
		||||
  const { accounts } = useSnapshot(state)
 | 
			
		||||
  const [dialogAccount, setDialogAccount] = useState<string | null>(null)
 | 
			
		||||
  if (!str || !accounts.length) return <>{str}</>
 | 
			
		||||
 | 
			
		||||
  const addrs = accounts.map(acc => acc.address)
 | 
			
		||||
  const regex = `(${addrs.join('|')}|HookSet\\(\\d+\\))`
 | 
			
		||||
  const res = regexifyString({
 | 
			
		||||
    pattern: new RegExp(regex, 'gim'),
 | 
			
		||||
    decorator: (match, idx) => {
 | 
			
		||||
      if (match.startsWith('r')) {
 | 
			
		||||
        // Account
 | 
			
		||||
        const name = accounts.find(acc => acc.address === match)?.name
 | 
			
		||||
        return (
 | 
			
		||||
          <Link
 | 
			
		||||
            key={match + idx}
 | 
			
		||||
            as="a"
 | 
			
		||||
            onClick={() => setDialogAccount(match)}
 | 
			
		||||
            title={match}
 | 
			
		||||
            highlighted
 | 
			
		||||
          >
 | 
			
		||||
            {name || match}
 | 
			
		||||
          </Link>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      if (match.startsWith('HookSet')) {
 | 
			
		||||
        const code = match.match(/^HookSet\((\d+)\)/)?.[1]
 | 
			
		||||
        const val = hookSetCodes.find(v => code && v.code === +code)
 | 
			
		||||
        console.log({ code, val })
 | 
			
		||||
        if (!val) return match
 | 
			
		||||
 | 
			
		||||
        const content = capitalize(val.description) || 'No hint available!'
 | 
			
		||||
        return (
 | 
			
		||||
          <>
 | 
			
		||||
            HookSet(
 | 
			
		||||
            <Tooltip content={content}>
 | 
			
		||||
              <Link>{val.identifier}</Link>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
            )
 | 
			
		||||
          </>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      return match
 | 
			
		||||
    },
 | 
			
		||||
    input: str
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {res}
 | 
			
		||||
      <AccountDialog
 | 
			
		||||
        setActiveAccountAddress={setDialogAccount}
 | 
			
		||||
        activeAccountAddress={dialogAccount}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default EnrichLog
 | 
			
		||||
@@ -1,8 +1,53 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import Box from './Box'
 | 
			
		||||
 | 
			
		||||
const Flex = styled(Box, {
 | 
			
		||||
  display: "flex",
 | 
			
		||||
});
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  variants: {
 | 
			
		||||
    row: {
 | 
			
		||||
      true: {
 | 
			
		||||
        flexDirection: 'row'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    column: {
 | 
			
		||||
      true: {
 | 
			
		||||
        flexDirection: 'column'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    fluid: {
 | 
			
		||||
      true: {
 | 
			
		||||
        width: '100%'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    align: {
 | 
			
		||||
      start: {
 | 
			
		||||
        alignItems: 'start'
 | 
			
		||||
      },
 | 
			
		||||
      center: {
 | 
			
		||||
        alignItems: 'center'
 | 
			
		||||
      },
 | 
			
		||||
      end: {
 | 
			
		||||
        alignItems: 'end'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    justify: {
 | 
			
		||||
      start: {
 | 
			
		||||
        justifyContent: 'start'
 | 
			
		||||
      },
 | 
			
		||||
      center: {
 | 
			
		||||
        justifyContent: 'center'
 | 
			
		||||
      },
 | 
			
		||||
      end: {
 | 
			
		||||
        justifyContent: 'end'
 | 
			
		||||
      },
 | 
			
		||||
      'space-between': {
 | 
			
		||||
        justifyContent: 'space-between'
 | 
			
		||||
      },
 | 
			
		||||
      'space-around': {
 | 
			
		||||
        justifyContent: 'space-around'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Flex;
 | 
			
		||||
export default Flex
 | 
			
		||||
 
 | 
			
		||||
@@ -1,51 +0,0 @@
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
 | 
			
		||||
import LogText from "./LogText";
 | 
			
		||||
import { state } from "../state";
 | 
			
		||||
 | 
			
		||||
const Footer = () => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      as="footer"
 | 
			
		||||
      css={{
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        borderTop: "1px solid $slate6",
 | 
			
		||||
        background: "$slate1",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Container css={{ py: "$4", flexShrink: 1 }}>
 | 
			
		||||
        <Box
 | 
			
		||||
          as="pre"
 | 
			
		||||
          css={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            flexDirection: "column",
 | 
			
		||||
            width: "100%",
 | 
			
		||||
            height: "160px",
 | 
			
		||||
            fontSize: "13px",
 | 
			
		||||
            fontWeight: "$body",
 | 
			
		||||
            fontFamily: "$monospace",
 | 
			
		||||
            overflowY: "scroll",
 | 
			
		||||
            wordWrap: "break-word",
 | 
			
		||||
            py: 3,
 | 
			
		||||
            px: 3,
 | 
			
		||||
            m: 3,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {snap.logs.map((log, index) => (
 | 
			
		||||
            <Box key={log.type + index}>
 | 
			
		||||
              <LogText capitalize variant={log.type}>
 | 
			
		||||
                {log.type}:{" "}
 | 
			
		||||
              </LogText>
 | 
			
		||||
              <LogText>{log.message}</LogText>
 | 
			
		||||
            </Box>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Footer;
 | 
			
		||||
@@ -1,10 +1,16 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const Heading = styled("span", {
 | 
			
		||||
  fontFamily: "$heading",
 | 
			
		||||
  lineHeight: "$heading",
 | 
			
		||||
  fontWeight: "$heading",
 | 
			
		||||
  textTransform: "uppercase",
 | 
			
		||||
});
 | 
			
		||||
const Heading = styled('span', {
 | 
			
		||||
  fontFamily: '$heading',
 | 
			
		||||
  lineHeight: '$heading',
 | 
			
		||||
  fontWeight: '$heading',
 | 
			
		||||
  variants: {
 | 
			
		||||
    uppercase: {
 | 
			
		||||
      true: {
 | 
			
		||||
        textTransform: 'uppercase'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Heading;
 | 
			
		||||
export default Heading
 | 
			
		||||
 
 | 
			
		||||
@@ -1,98 +1,333 @@
 | 
			
		||||
import React, { useEffect, useRef } from "react";
 | 
			
		||||
import { useSnapshot, ref } from "valtio";
 | 
			
		||||
import Editor from "@monaco-editor/react";
 | 
			
		||||
import type monaco from "monaco-editor";
 | 
			
		||||
import { Play } from "phosphor-react";
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import React, { useEffect, useRef, useState } from 'react'
 | 
			
		||||
import { useSnapshot, ref } from 'valtio'
 | 
			
		||||
import type monaco from 'monaco-editor'
 | 
			
		||||
import { ArrowBendLeftUp } from 'phosphor-react'
 | 
			
		||||
import { useTheme } from 'next-themes'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import dark from "../theme/editor/amy.json";
 | 
			
		||||
import light from "../theme/editor/xcode_default.json";
 | 
			
		||||
import { compileCode, saveFile, state } from "../state";
 | 
			
		||||
import Box from './Box'
 | 
			
		||||
import Container from './Container'
 | 
			
		||||
import { createNewFile, saveFile } from '../state/actions'
 | 
			
		||||
import { apiHeaderFiles } from '../state/constants'
 | 
			
		||||
import state from '../state'
 | 
			
		||||
 | 
			
		||||
import EditorNavigation from "./EditorNavigation";
 | 
			
		||||
import Spinner from "./Spinner";
 | 
			
		||||
import EditorNavigation from './EditorNavigation'
 | 
			
		||||
import Text from './Text'
 | 
			
		||||
import { MonacoServices } from '@codingame/monaco-languageclient'
 | 
			
		||||
import { createLanguageClient, createWebSocket } from '../utils/languageClient'
 | 
			
		||||
import { listen } from '@codingame/monaco-jsonrpc'
 | 
			
		||||
import ReconnectingWebSocket from 'reconnecting-websocket'
 | 
			
		||||
 | 
			
		||||
import docs from '../xrpl-hooks-docs/docs'
 | 
			
		||||
import Monaco from './Monaco'
 | 
			
		||||
import { saveAllFiles } from '../state/actions/saveFile'
 | 
			
		||||
import { Tab, Tabs } from './Tabs'
 | 
			
		||||
import { renameFile } from '../state/actions/createNewFile'
 | 
			
		||||
import { Link } from '.'
 | 
			
		||||
import Markdown from './Markdown'
 | 
			
		||||
 | 
			
		||||
const checkWritable = (filename?: string): boolean => {
 | 
			
		||||
  if (apiHeaderFiles.find(file => file === filename)) {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
 | 
			
		||||
  const filename = editor.getModel()?.uri.path.split('/').pop()
 | 
			
		||||
  const isWritable = checkWritable(filename)
 | 
			
		||||
  editor.updateOptions({ readOnly: !isWritable })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let decorations: { [key: string]: string[] } = {}
 | 
			
		||||
 | 
			
		||||
const setMarkers = (monacoE: typeof monaco) => {
 | 
			
		||||
  // Get all the markers that are active at the moment,
 | 
			
		||||
  // Also if same error is there twice, we can show the content
 | 
			
		||||
  // only once (that's why we're using uniqBy)
 | 
			
		||||
  const markers = monacoE.editor
 | 
			
		||||
    .getModelMarkers({})
 | 
			
		||||
    // Filter out the markers that are hooks specific
 | 
			
		||||
    .filter(
 | 
			
		||||
      marker =>
 | 
			
		||||
        typeof marker?.code === 'string' &&
 | 
			
		||||
        // Take only markers that starts with "hooks-"
 | 
			
		||||
        marker?.code?.includes('hooks-')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  // Get the active model (aka active file you're editing)
 | 
			
		||||
  // const model = monacoE.editor?.getModel(
 | 
			
		||||
  //   monacoE.Uri.parse(`file:///work/c/${state.files?.[state.active]?.name}`)
 | 
			
		||||
  // );
 | 
			
		||||
  // console.log(state.active);
 | 
			
		||||
  // Add decoration (aka extra hoverMessages) to markers in the
 | 
			
		||||
  // exact same range (location) where the markers are
 | 
			
		||||
  const models = monacoE.editor.getModels()
 | 
			
		||||
  models.forEach(model => {
 | 
			
		||||
    decorations[model.id] = model?.deltaDecorations(
 | 
			
		||||
      decorations?.[model.id] || [],
 | 
			
		||||
      markers
 | 
			
		||||
        .filter(marker =>
 | 
			
		||||
          marker?.resource.path.split('/').includes(`${state.files?.[state.active]?.name}`)
 | 
			
		||||
        )
 | 
			
		||||
        .map(marker => ({
 | 
			
		||||
          range: new monacoE.Range(
 | 
			
		||||
            marker.startLineNumber,
 | 
			
		||||
            marker.startColumn,
 | 
			
		||||
            marker.endLineNumber,
 | 
			
		||||
            marker.endColumn
 | 
			
		||||
          ),
 | 
			
		||||
          options: {
 | 
			
		||||
            hoverMessage: {
 | 
			
		||||
              value:
 | 
			
		||||
                // Find the related hover message markdown from the
 | 
			
		||||
                // /xrpl-hooks-docs/xrpl-hooks-docs-files.json file
 | 
			
		||||
                // which was generated from rst files
 | 
			
		||||
 | 
			
		||||
                (typeof marker.code === 'string' && docs[marker?.code]?.toString()) || '',
 | 
			
		||||
              supportHtml: true,
 | 
			
		||||
              isTrusted: true
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }))
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const HooksEditor = () => {
 | 
			
		||||
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const { theme } = useTheme();
 | 
			
		||||
  // useEffect(() => {
 | 
			
		||||
  //   if (snap.editorCtx) {
 | 
			
		||||
  //     snap.editorCtx.getModels().forEach((model) => {
 | 
			
		||||
  //       // console.log(model.id,);
 | 
			
		||||
  //       snap.editorCtx?.createModel(model.getValue(), "c", model.uri);
 | 
			
		||||
  //     });
 | 
			
		||||
  //   }
 | 
			
		||||
  //   // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  // }, []);
 | 
			
		||||
  console.log("reinit");
 | 
			
		||||
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>()
 | 
			
		||||
  const monacoRef = useRef<typeof monaco>()
 | 
			
		||||
  const subscriptionRef = useRef<ReconnectingWebSocket | null>(null)
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const [isMdPreview, setIsMdPreview] = useState(true)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (editorRef.current) validateWritability(editorRef.current)
 | 
			
		||||
  }, [snap.active])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    return () => {
 | 
			
		||||
      subscriptionRef?.current?.close()
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (monacoRef.current) {
 | 
			
		||||
      setMarkers(monacoRef.current)
 | 
			
		||||
    }
 | 
			
		||||
  }, [snap.active])
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    return () => {
 | 
			
		||||
      saveAllFiles()
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const file = snap.files[snap.active]
 | 
			
		||||
 | 
			
		||||
  const renderNav = () => (
 | 
			
		||||
    <Tabs
 | 
			
		||||
      label="File"
 | 
			
		||||
      activeIndex={snap.active}
 | 
			
		||||
      onChangeActive={idx => (state.active = idx)}
 | 
			
		||||
      extensionRequired
 | 
			
		||||
      onCreateNewTab={createNewFile}
 | 
			
		||||
      onCloseTab={idx => state.files.splice(idx, 1)}
 | 
			
		||||
      onRenameTab={(idx, nwName, oldName = '') => renameFile(oldName, nwName)}
 | 
			
		||||
      headerExtraValidation={{
 | 
			
		||||
        regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
 | 
			
		||||
        error: 'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {snap.files.map((file, index) => {
 | 
			
		||||
        return <Tab key={file.name} header={file.name} renameDisabled={!checkWritable(file.name)} />
 | 
			
		||||
      })}
 | 
			
		||||
    </Tabs>
 | 
			
		||||
  )
 | 
			
		||||
  const previewToggle = (
 | 
			
		||||
    <Link
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        if (!isMdPreview) {
 | 
			
		||||
          saveFile(false)
 | 
			
		||||
        }
 | 
			
		||||
        setIsMdPreview(!isMdPreview)
 | 
			
		||||
      }}
 | 
			
		||||
      css={{
 | 
			
		||||
        position: 'absolute',
 | 
			
		||||
        right: 0,
 | 
			
		||||
        bottom: 0,
 | 
			
		||||
        zIndex: 10,
 | 
			
		||||
        m: '$1',
 | 
			
		||||
        fontSize: '$sm'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {isMdPreview ? 'Exit Preview' : 'View Preview'}
 | 
			
		||||
    </Link>
 | 
			
		||||
  )
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      css={{
 | 
			
		||||
        flex: 1,
 | 
			
		||||
        flexShrink: 1,
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        position: "relative",
 | 
			
		||||
        flexDirection: "column",
 | 
			
		||||
        backgroundColor: "$slate3",
 | 
			
		||||
        width: "100%",
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        flexDirection: 'column',
 | 
			
		||||
        backgroundColor: '$mauve2',
 | 
			
		||||
        width: '100%'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <EditorNavigation />
 | 
			
		||||
      <Editor
 | 
			
		||||
        keepCurrentModel
 | 
			
		||||
        // defaultLanguage={snap.files?.[snap.active]?.language}
 | 
			
		||||
        path={snap.files?.[snap.active]?.name}
 | 
			
		||||
        // defaultValue={snap.files?.[snap.active]?.content}
 | 
			
		||||
        beforeMount={(monaco) => {
 | 
			
		||||
          if (!state.editorCtx) {
 | 
			
		||||
            state.editorCtx = ref(monaco.editor);
 | 
			
		||||
            // @ts-expect-error
 | 
			
		||||
            monaco.editor.defineTheme("dark", dark);
 | 
			
		||||
            // @ts-expect-error
 | 
			
		||||
            monaco.editor.defineTheme("light", light);
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        onMount={(editor, monaco) => {
 | 
			
		||||
          editorRef.current = editor;
 | 
			
		||||
          // hook editor to global state
 | 
			
		||||
          editor.updateOptions({
 | 
			
		||||
            minimap: {
 | 
			
		||||
              enabled: false,
 | 
			
		||||
            },
 | 
			
		||||
            ...snap.editorSettings,
 | 
			
		||||
          });
 | 
			
		||||
          editor.addCommand(
 | 
			
		||||
            monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S,
 | 
			
		||||
            () => {
 | 
			
		||||
              saveFile(editor.getValue());
 | 
			
		||||
            }
 | 
			
		||||
          );
 | 
			
		||||
        }}
 | 
			
		||||
        theme={theme === "dark" ? "dark" : "light"}
 | 
			
		||||
      />
 | 
			
		||||
      <Button
 | 
			
		||||
        variant="primary"
 | 
			
		||||
        uppercase
 | 
			
		||||
        onClick={() => compileCode(snap.active)}
 | 
			
		||||
        disabled={snap.compiling}
 | 
			
		||||
        css={{
 | 
			
		||||
          position: "absolute",
 | 
			
		||||
          bottom: "$4",
 | 
			
		||||
          left: "$4",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          cursor: "pointer",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Play weight="bold" size="16px" />
 | 
			
		||||
        Compile to Wasm
 | 
			
		||||
        {snap.compiling && <Spinner />}
 | 
			
		||||
      </Button>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
      <EditorNavigation renderNav={renderNav} />
 | 
			
		||||
      {file?.language === 'markdown' && previewToggle}
 | 
			
		||||
      {snap.files.length > 0 && router.isReady ? (
 | 
			
		||||
        isMdPreview && file?.language === 'markdown' ? (
 | 
			
		||||
          <Markdown
 | 
			
		||||
            components={{
 | 
			
		||||
              a: ({ href, children }) => (
 | 
			
		||||
                <Link target="_blank" rel="noopener noreferrer" href={href}>
 | 
			
		||||
                  {children}
 | 
			
		||||
                </Link>
 | 
			
		||||
              )
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {file.content}
 | 
			
		||||
          </Markdown>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Monaco
 | 
			
		||||
            keepCurrentModel
 | 
			
		||||
            defaultLanguage={file?.language}
 | 
			
		||||
            language={file?.language}
 | 
			
		||||
            path={`file:///work/c/${file?.name}`}
 | 
			
		||||
            defaultValue={file?.content}
 | 
			
		||||
            // onChange={val => (state.files[snap.active].content = val)} // Auto save?
 | 
			
		||||
            beforeMount={monaco => {
 | 
			
		||||
              // if (!snap.editorCtx) {
 | 
			
		||||
              //   snap.files.forEach(file =>
 | 
			
		||||
              //     monaco.editor.createModel(
 | 
			
		||||
              //       file.content,
 | 
			
		||||
              //       file.language,
 | 
			
		||||
              //       monaco.Uri.parse(`file:///work/c/${file.name}`)
 | 
			
		||||
              //     )
 | 
			
		||||
              //   )
 | 
			
		||||
              // }
 | 
			
		||||
 | 
			
		||||
export default HooksEditor;
 | 
			
		||||
              // create the web socket
 | 
			
		||||
              if (!subscriptionRef.current) {
 | 
			
		||||
                monaco.languages.register({
 | 
			
		||||
                  id: 'c',
 | 
			
		||||
                  extensions: ['.c', '.h'],
 | 
			
		||||
                  aliases: ['C', 'c', 'H', 'h'],
 | 
			
		||||
                  mimetypes: ['text/plain']
 | 
			
		||||
                })
 | 
			
		||||
                monaco.languages.register({
 | 
			
		||||
                  id: 'text',
 | 
			
		||||
                  extensions: ['.txt'],
 | 
			
		||||
                  mimetypes: ['text/plain'],
 | 
			
		||||
                })
 | 
			
		||||
                MonacoServices.install(monaco)
 | 
			
		||||
                const webSocket = createWebSocket(
 | 
			
		||||
                  process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ''
 | 
			
		||||
                )
 | 
			
		||||
                subscriptionRef.current = webSocket
 | 
			
		||||
                // listen when the web socket is opened
 | 
			
		||||
                listen({
 | 
			
		||||
                  webSocket: webSocket as WebSocket,
 | 
			
		||||
                  onConnection: connection => {
 | 
			
		||||
                    // create and start the language client
 | 
			
		||||
                    const languageClient = createLanguageClient(connection)
 | 
			
		||||
                    const disposable = languageClient.start()
 | 
			
		||||
 | 
			
		||||
                    connection.onClose(() => {
 | 
			
		||||
                      try {
 | 
			
		||||
                        disposable.dispose()
 | 
			
		||||
                      } catch (err) {
 | 
			
		||||
                        console.log('err', err)
 | 
			
		||||
                      }
 | 
			
		||||
                    })
 | 
			
		||||
                  }
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              // editor.updateOptions({
 | 
			
		||||
              //   minimap: {
 | 
			
		||||
              //     enabled: false,
 | 
			
		||||
              //   },
 | 
			
		||||
              //   ...snap.editorSettings,
 | 
			
		||||
              // });
 | 
			
		||||
              if (!state.editorCtx) {
 | 
			
		||||
                state.editorCtx = ref(monaco.editor)
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            onMount={(editor, monaco) => {
 | 
			
		||||
              editorRef.current = editor
 | 
			
		||||
              monacoRef.current = monaco
 | 
			
		||||
              editor.updateOptions({
 | 
			
		||||
                glyphMargin: true,
 | 
			
		||||
                lightbulb: {
 | 
			
		||||
                  enabled: true
 | 
			
		||||
                }
 | 
			
		||||
              })
 | 
			
		||||
              editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
 | 
			
		||||
                saveFile()
 | 
			
		||||
              })
 | 
			
		||||
              // When the markers (errors/warnings from clangd language server) change
 | 
			
		||||
              // Lets improve the markers by adding extra content to them from related
 | 
			
		||||
              // md files
 | 
			
		||||
              monaco.editor.onDidChangeMarkers(() => {
 | 
			
		||||
                if (monacoRef.current) {
 | 
			
		||||
                  setMarkers(monacoRef.current)
 | 
			
		||||
                }
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
              // Hacky way to hide Peek menu
 | 
			
		||||
              editor.onContextMenu(e => {
 | 
			
		||||
                const host = document.querySelector<HTMLElement>('.shadow-root-host')
 | 
			
		||||
 | 
			
		||||
                const contextMenuItems = host?.shadowRoot?.querySelectorAll('li.action-item')
 | 
			
		||||
                contextMenuItems?.forEach(k => {
 | 
			
		||||
                  // If menu item contains "Peek" lets hide it
 | 
			
		||||
                  if (k.querySelector('.action-label')?.textContent === 'Peek') {
 | 
			
		||||
                    // @ts-expect-error
 | 
			
		||||
                    k['style'].display = 'none'
 | 
			
		||||
                  }
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
              validateWritability(editor)
 | 
			
		||||
            }}
 | 
			
		||||
            theme={theme === 'dark' ? 'dark' : 'light'}
 | 
			
		||||
          />
 | 
			
		||||
        )
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Container>
 | 
			
		||||
          {!snap.loading && router.isReady && (
 | 
			
		||||
            <Box
 | 
			
		||||
              css={{
 | 
			
		||||
                flexDirection: 'row',
 | 
			
		||||
                width: '$spaces$wide',
 | 
			
		||||
                gap: '$3',
 | 
			
		||||
                display: 'inline-flex'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Box css={{ display: 'inline-flex', pl: '35px' }}>
 | 
			
		||||
                <ArrowBendLeftUp size={30} />
 | 
			
		||||
              </Box>
 | 
			
		||||
              <Box css={{ pl: '0px', pt: '15px', flex: 1, display: 'inline-flex' }}>
 | 
			
		||||
                <Text
 | 
			
		||||
                  css={{
 | 
			
		||||
                    fontSize: '14px',
 | 
			
		||||
                    maxWidth: '220px',
 | 
			
		||||
                    fontFamily: '$monospace'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  Click the link above to create your file
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Box>
 | 
			
		||||
            </Box>
 | 
			
		||||
          )}
 | 
			
		||||
        </Container>
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default HooksEditor
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										165
									
								
								components/Input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,165 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import * as LabelPrim from '@radix-ui/react-label'
 | 
			
		||||
 | 
			
		||||
export const Input = styled('input', {
 | 
			
		||||
  // Reset
 | 
			
		||||
  appearance: 'none',
 | 
			
		||||
  borderWidth: '0',
 | 
			
		||||
  boxSizing: 'border-box',
 | 
			
		||||
  fontFamily: 'inherit',
 | 
			
		||||
  outline: 'none',
 | 
			
		||||
  width: '100%',
 | 
			
		||||
  flex: '1',
 | 
			
		||||
  backgroundColor: '$mauve4',
 | 
			
		||||
  display: 'inline-flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'center',
 | 
			
		||||
  borderRadius: '$sm',
 | 
			
		||||
  px: '$2',
 | 
			
		||||
  fontSize: '$md',
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: '$mauve12',
 | 
			
		||||
  boxShadow: `0 0 0 1px $colors$mauve8`,
 | 
			
		||||
  height: 35,
 | 
			
		||||
  WebkitTapHighlightColor: 'rgba(0,0,0,0)',
 | 
			
		||||
  '&::before': {
 | 
			
		||||
    boxSizing: 'border-box'
 | 
			
		||||
  },
 | 
			
		||||
  '&::after': {
 | 
			
		||||
    boxSizing: 'border-box'
 | 
			
		||||
  },
 | 
			
		||||
  fontVariantNumeric: 'tabular-nums',
 | 
			
		||||
 | 
			
		||||
  '&:-webkit-autofill': {
 | 
			
		||||
    boxShadow: 'inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3'
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  '&:-webkit-autofill::first-line': {
 | 
			
		||||
    fontFamily: '$untitled',
 | 
			
		||||
    color: '$mauve12'
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  '&:focus': {
 | 
			
		||||
    boxShadow: `0 0 0 1px $colors$mauve10`,
 | 
			
		||||
    '&:-webkit-autofill': {
 | 
			
		||||
      boxShadow: `0 0 0 1px $colors$mauve10`
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  '&::placeholder': {
 | 
			
		||||
    color: '$mauve9'
 | 
			
		||||
  },
 | 
			
		||||
  '&:disabled': {
 | 
			
		||||
    pointerEvents: 'none',
 | 
			
		||||
    backgroundColor: '$mauve2',
 | 
			
		||||
    color: '$mauve8',
 | 
			
		||||
    cursor: 'not-allowed',
 | 
			
		||||
    '&::placeholder': {
 | 
			
		||||
      color: '$mauve7'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  '&:read-only': {
 | 
			
		||||
    backgroundColor: '$mauve2',
 | 
			
		||||
    color: '$text',
 | 
			
		||||
    opacity: 0.8,
 | 
			
		||||
    '&:focus': {
 | 
			
		||||
      boxShadow: 'inset 0px 0px 0px 1px $colors$mauve7'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  variants: {
 | 
			
		||||
    size: {
 | 
			
		||||
      sm: {
 | 
			
		||||
        height: '$5',
 | 
			
		||||
        fontSize: '$1',
 | 
			
		||||
        lineHeight: '$sizes$4',
 | 
			
		||||
        '&:-webkit-autofill::first-line': {
 | 
			
		||||
          fontSize: '$1'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      md: {
 | 
			
		||||
        height: '$8',
 | 
			
		||||
        fontSize: '$1',
 | 
			
		||||
        lineHeight: '$sizes$5',
 | 
			
		||||
        '&:-webkit-autofill::first-line': {
 | 
			
		||||
          fontSize: '$1'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      lg: {
 | 
			
		||||
        height: '$12',
 | 
			
		||||
        fontSize: '$2',
 | 
			
		||||
        lineHeight: '$sizes$6',
 | 
			
		||||
        '&:-webkit-autofill::first-line': {
 | 
			
		||||
          fontSize: '$3'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    variant: {
 | 
			
		||||
      ghost: {
 | 
			
		||||
        boxShadow: 'none',
 | 
			
		||||
        backgroundColor: 'transparent',
 | 
			
		||||
        '@hover': {
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$mauve7'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          backgroundColor: '$loContrast',
 | 
			
		||||
          boxShadow: `0 0 0 1px $colors$mauve10`
 | 
			
		||||
        },
 | 
			
		||||
        '&:disabled': {
 | 
			
		||||
          backgroundColor: 'transparent'
 | 
			
		||||
        },
 | 
			
		||||
        '&:read-only': {
 | 
			
		||||
          backgroundColor: 'transparent'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      deep: {
 | 
			
		||||
        backgroundColor: '$deep',
 | 
			
		||||
        boxShadow: 'none'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    state: {
 | 
			
		||||
      invalid: {
 | 
			
		||||
        boxShadow: 'inset 0 0 0 1px $colors$crimson7',
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          boxShadow: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      valid: {
 | 
			
		||||
        boxShadow: 'inset 0 0 0 1px $colors$grass7',
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          boxShadow: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    cursor: {
 | 
			
		||||
      default: {
 | 
			
		||||
        cursor: 'default',
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          cursor: 'text'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      text: {
 | 
			
		||||
        cursor: 'text'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  defaultVariants: {
 | 
			
		||||
    size: 'md'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line react/display-name
 | 
			
		||||
const ReffedInput = React.forwardRef<HTMLInputElement, React.ComponentProps<typeof Input>>(
 | 
			
		||||
  (props, ref) => <Input {...props} ref={ref} />
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default ReffedInput
 | 
			
		||||
 | 
			
		||||
const LabelRoot = (props: LabelPrim.LabelProps) => <LabelPrim.Root {...props} />
 | 
			
		||||
 | 
			
		||||
export const Label = styled(LabelRoot, {
 | 
			
		||||
  display: 'inline-block',
 | 
			
		||||
  mb: '$1'
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										16
									
								
								components/Link.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const StyledLink = styled('a', {
 | 
			
		||||
  color: 'CurrentColor',
 | 
			
		||||
  textDecoration: 'underline',
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
  variants: {
 | 
			
		||||
    highlighted: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: '$blue9'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default StyledLink
 | 
			
		||||
							
								
								
									
										178
									
								
								components/LogBox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,178 @@
 | 
			
		||||
import { useRef, useLayoutEffect, ReactNode, FC, useState } from 'react'
 | 
			
		||||
import { IconProps, Notepad, Prohibit } from 'phosphor-react'
 | 
			
		||||
import useStayScrolled from 'react-stay-scrolled'
 | 
			
		||||
import NextLink from 'next/link'
 | 
			
		||||
 | 
			
		||||
import Container from './Container'
 | 
			
		||||
import LogText from './LogText'
 | 
			
		||||
import { ILog } from '../state'
 | 
			
		||||
import { Pre, Link, Heading, Button, Text, Flex, Box } from '.'
 | 
			
		||||
 | 
			
		||||
interface ILogBox {
 | 
			
		||||
  title: string
 | 
			
		||||
  clearLog?: () => void
 | 
			
		||||
  logs: ILog[]
 | 
			
		||||
  renderNav?: () => ReactNode
 | 
			
		||||
  enhanced?: boolean
 | 
			
		||||
  Icon?: FC<IconProps>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const LogBox: FC<ILogBox> = ({
 | 
			
		||||
  title,
 | 
			
		||||
  clearLog,
 | 
			
		||||
  logs,
 | 
			
		||||
  children,
 | 
			
		||||
  renderNav,
 | 
			
		||||
  enhanced,
 | 
			
		||||
  Icon = Notepad
 | 
			
		||||
}) => {
 | 
			
		||||
  const logRef = useRef<HTMLPreElement>(null)
 | 
			
		||||
  const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef)
 | 
			
		||||
 | 
			
		||||
  useLayoutEffect(() => {
 | 
			
		||||
    stayScrolled()
 | 
			
		||||
  }, [stayScrolled, logs])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Flex
 | 
			
		||||
      as="div"
 | 
			
		||||
      css={{
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        borderTop: '1px solid $mauve6',
 | 
			
		||||
        background: '$mauve1',
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        flex: 1,
 | 
			
		||||
        height: '100%'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Container
 | 
			
		||||
        css={{
 | 
			
		||||
          px: 0,
 | 
			
		||||
          height: '100%'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Flex
 | 
			
		||||
          fluid
 | 
			
		||||
          css={{
 | 
			
		||||
            height: '48px',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            fontSize: '$sm',
 | 
			
		||||
            fontWeight: 300
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Heading
 | 
			
		||||
            as="h3"
 | 
			
		||||
            css={{
 | 
			
		||||
              fontWeight: 300,
 | 
			
		||||
              m: 0,
 | 
			
		||||
              fontSize: '11px',
 | 
			
		||||
              color: '$mauve12',
 | 
			
		||||
              px: '$3',
 | 
			
		||||
              textTransform: 'uppercase',
 | 
			
		||||
              alignItems: 'center',
 | 
			
		||||
              display: 'inline-flex',
 | 
			
		||||
              gap: '$3'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
 | 
			
		||||
          </Heading>
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            align="center"
 | 
			
		||||
            // css={{
 | 
			
		||||
            //   maxWidth: "100%", // TODO make it max without breaking layout!
 | 
			
		||||
            // }}
 | 
			
		||||
          >
 | 
			
		||||
            {renderNav?.()}
 | 
			
		||||
          </Flex>
 | 
			
		||||
          <Flex css={{ ml: 'auto', gap: '$3', marginRight: '$3' }}>
 | 
			
		||||
            {clearLog && (
 | 
			
		||||
              <Button ghost size="xs" onClick={clearLog}>
 | 
			
		||||
                <Prohibit size="14px" />
 | 
			
		||||
              </Button>
 | 
			
		||||
            )}
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </Flex>
 | 
			
		||||
 | 
			
		||||
        <Box
 | 
			
		||||
          as="pre"
 | 
			
		||||
          ref={logRef}
 | 
			
		||||
          css={{
 | 
			
		||||
            margin: 0,
 | 
			
		||||
            // display: "inline-block",
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            flexDirection: 'column',
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            height: 'calc(100% - 48px)', // 100% minus the logbox header height
 | 
			
		||||
            overflowY: 'auto',
 | 
			
		||||
            fontSize: '13px',
 | 
			
		||||
            fontWeight: '$body',
 | 
			
		||||
            fontFamily: '$monospace',
 | 
			
		||||
            px: '$3',
 | 
			
		||||
            pb: '$2',
 | 
			
		||||
            whiteSpace: 'normal'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {logs?.map((log, index) => (
 | 
			
		||||
            <Box
 | 
			
		||||
              as="span"
 | 
			
		||||
              key={log.type + index}
 | 
			
		||||
              css={{
 | 
			
		||||
                '@hover': {
 | 
			
		||||
                  '&:hover': {
 | 
			
		||||
                    backgroundColor: enhanced ? '$backgroundAlt' : undefined
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
                p: enhanced ? '$1' : undefined,
 | 
			
		||||
                my: enhanced ? '$1' : undefined
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Log {...log} />
 | 
			
		||||
            </Box>
 | 
			
		||||
          ))}
 | 
			
		||||
          {children}
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Log: FC<ILog> = ({
 | 
			
		||||
  type,
 | 
			
		||||
  timestring,
 | 
			
		||||
  message,
 | 
			
		||||
  link,
 | 
			
		||||
  linkText,
 | 
			
		||||
  defaultCollapsed,
 | 
			
		||||
  jsonData
 | 
			
		||||
}) => {
 | 
			
		||||
  const [expanded, setExpanded] = useState(!defaultCollapsed)
 | 
			
		||||
 | 
			
		||||
  if (message === undefined) message = <Text muted>{'undefined'}</Text>
 | 
			
		||||
  else if (message === '') message = <Text muted>{'""'}</Text>
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <LogText variant={type}>
 | 
			
		||||
        {timestring && (
 | 
			
		||||
          <Text muted monospace>
 | 
			
		||||
            {timestring}{' '}
 | 
			
		||||
          </Text>
 | 
			
		||||
        )}
 | 
			
		||||
        <Pre>{message}</Pre>
 | 
			
		||||
        {link && (
 | 
			
		||||
          <NextLink href={link} shallow passHref>
 | 
			
		||||
            <Link as="a">{linkText}</Link>
 | 
			
		||||
          </NextLink>
 | 
			
		||||
        )}
 | 
			
		||||
        {jsonData && (
 | 
			
		||||
          <Link onClick={() => setExpanded(!expanded)} as="a">
 | 
			
		||||
            {expanded ? 'Collapse' : 'Expand'}
 | 
			
		||||
          </Link>
 | 
			
		||||
        )}
 | 
			
		||||
        {expanded && jsonData && <Pre block>{jsonData}</Pre>}
 | 
			
		||||
      </LogText>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default LogBox
 | 
			
		||||
@@ -1,27 +1,31 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const Text = styled("span", {
 | 
			
		||||
  fontFamily: "$monospace",
 | 
			
		||||
  lineHeight: "$body",
 | 
			
		||||
  color: "$text",
 | 
			
		||||
const Text = styled('span', {
 | 
			
		||||
  fontFamily: '$monospace',
 | 
			
		||||
  lineHeight: '$body',
 | 
			
		||||
  color: '$text',
 | 
			
		||||
  wordWrap: 'break-word',
 | 
			
		||||
  variants: {
 | 
			
		||||
    variant: {
 | 
			
		||||
      log: {
 | 
			
		||||
        color: "$text",
 | 
			
		||||
        color: '$text'
 | 
			
		||||
      },
 | 
			
		||||
      warning: {
 | 
			
		||||
        color: "$yellow11",
 | 
			
		||||
        color: '$warning'
 | 
			
		||||
      },
 | 
			
		||||
      error: {
 | 
			
		||||
        color: "$red11",
 | 
			
		||||
        color: '$error'
 | 
			
		||||
      },
 | 
			
		||||
      success: {
 | 
			
		||||
        color: '$success'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    capitalize: {
 | 
			
		||||
      true: {
 | 
			
		||||
        textTransform: "capitalize",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
        textTransform: 'capitalize'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Text;
 | 
			
		||||
export default Text
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +1,28 @@
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const SVG = styled("svg", {
 | 
			
		||||
  "& #path-one, & #path-two": {
 | 
			
		||||
    fill: "$text",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
function Logo({
 | 
			
		||||
  width,
 | 
			
		||||
  height,
 | 
			
		||||
}: {
 | 
			
		||||
  width?: string | number;
 | 
			
		||||
  height?: string | number;
 | 
			
		||||
}) {
 | 
			
		||||
const SVG = styled('svg', {
 | 
			
		||||
  '& #path': {
 | 
			
		||||
    fill: '$accent'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
function Logo({ width, height }: { width?: string | number; height?: string | number }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SVG
 | 
			
		||||
      width={width || "1em"}
 | 
			
		||||
      height={height || "1em"}
 | 
			
		||||
      viewBox="0 0 28 22"
 | 
			
		||||
      width={width || '1.1em'}
 | 
			
		||||
      height={height || '1.1em'}
 | 
			
		||||
      viewBox="0 0 294 283"
 | 
			
		||||
      fill="none"
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        id="path-one"
 | 
			
		||||
        d="M19.603 3.87h2.3l-4.786 4.747a4.466 4.466 0 01-6.276 0L6.054 3.871h2.3l3.636 3.605a2.828 2.828 0 003.975 0l3.638-3.605zM8.325 17.069h-2.3l4.816-4.776a4.466 4.466 0 016.276 0l4.816 4.776h-2.3l-3.665-3.635a2.828 2.828 0 00-3.975 0l-3.668 3.635z"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        id="path-two"
 | 
			
		||||
        fillRule="evenodd"
 | 
			
		||||
        clipRule="evenodd"
 | 
			
		||||
        d="M1.556 9.769h4.751v1.555H1.556v10.072H0V0h1.556v9.769zM26.444 9.769h-4.751v1.555h4.751v10.072H28V0h-1.556v9.769z"
 | 
			
		||||
        id="path"
 | 
			
		||||
        d="M265.827 235L172.416 141.589L265.005 49H226.822L147.732 128.089H53.5514L27.4824 155.089H147.732L227.643 235H265.827Z"
 | 
			
		||||
        fill="#9D2DFF"
 | 
			
		||||
      />
 | 
			
		||||
    </SVG>
 | 
			
		||||
  );
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Logo;
 | 
			
		||||
export default Logo
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								components/Markdown.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
			
		||||
import ReactMarkdown from 'react-markdown'
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const Markdown = styled(ReactMarkdown, {
 | 
			
		||||
  px: '$8',
 | 
			
		||||
  '@md': {
 | 
			
		||||
    px: '$20'
 | 
			
		||||
  },
 | 
			
		||||
  pb: '$5',
 | 
			
		||||
  height: '100%',
 | 
			
		||||
  overflowY: 'auto'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Markdown
 | 
			
		||||
							
								
								
									
										71
									
								
								components/Monaco.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,71 @@
 | 
			
		||||
import Editor, { loader, EditorProps, Monaco } from '@monaco-editor/react'
 | 
			
		||||
import { CSS } from '@stitches/react'
 | 
			
		||||
import type monaco from 'monaco-editor'
 | 
			
		||||
import { useTheme } from 'next-themes'
 | 
			
		||||
import { FC, MutableRefObject, ReactNode } from 'react'
 | 
			
		||||
import { Flex } from '.'
 | 
			
		||||
import dark from '../theme/editor/amy.json'
 | 
			
		||||
import light from '../theme/editor/xcode_default.json'
 | 
			
		||||
 | 
			
		||||
export type MonacoProps = EditorProps & {
 | 
			
		||||
  id?: string
 | 
			
		||||
  rootProps?: { css: CSS } & Record<string, any>
 | 
			
		||||
  overlay?: ReactNode
 | 
			
		||||
  editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor>
 | 
			
		||||
  monacoRef?: MutableRefObject<typeof monaco>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
loader.config({
 | 
			
		||||
  paths: {
 | 
			
		||||
    vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const Monaco: FC<MonacoProps> = ({
 | 
			
		||||
  id,
 | 
			
		||||
  path = `file:///${id}`,
 | 
			
		||||
  className = id,
 | 
			
		||||
  language = 'json',
 | 
			
		||||
  overlay,
 | 
			
		||||
  editorRef,
 | 
			
		||||
  monacoRef,
 | 
			
		||||
  beforeMount,
 | 
			
		||||
  rootProps,
 | 
			
		||||
  ...rest
 | 
			
		||||
}) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const setTheme = (monaco: Monaco) => {
 | 
			
		||||
    monaco.editor.defineTheme('dark', dark as any)
 | 
			
		||||
    monaco.editor.defineTheme('light', light as any)
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Flex
 | 
			
		||||
      fluid
 | 
			
		||||
      column
 | 
			
		||||
      {...rootProps}
 | 
			
		||||
      css={{
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        height: '100%',
 | 
			
		||||
        ...rootProps?.css
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Editor
 | 
			
		||||
        className={className}
 | 
			
		||||
        language={language}
 | 
			
		||||
        path={path}
 | 
			
		||||
        beforeMount={monaco => {
 | 
			
		||||
          beforeMount?.(monaco)
 | 
			
		||||
 | 
			
		||||
          setTheme(monaco)
 | 
			
		||||
        }}
 | 
			
		||||
        theme={theme === 'dark' ? 'dark' : 'light'}
 | 
			
		||||
        {...rest}
 | 
			
		||||
      />
 | 
			
		||||
      {overlay && (
 | 
			
		||||
        <Flex css={{ position: 'absolute', bottom: 0, right: 0, width: '100%' }}>{overlay}</Flex>
 | 
			
		||||
      )}
 | 
			
		||||
    </Flex>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Monaco
 | 
			
		||||
@@ -1,160 +1,369 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import {
 | 
			
		||||
  Gear,
 | 
			
		||||
  GithubLogo,
 | 
			
		||||
  SignOut,
 | 
			
		||||
  User,
 | 
			
		||||
  ArrowSquareOut,
 | 
			
		||||
} from "phosphor-react";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import Image from "next/image";
 | 
			
		||||
import { useSession, signIn, signOut } from "next-auth/react";
 | 
			
		||||
import {
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
  DropdownMenuTrigger,
 | 
			
		||||
  DropdownMenuContent,
 | 
			
		||||
  DropdownMenuItem,
 | 
			
		||||
  DropdownMenuArrow,
 | 
			
		||||
  DropdownMenuSeparator,
 | 
			
		||||
} from "./DropdownMenu";
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
 | 
			
		||||
import Stack from "./Stack";
 | 
			
		||||
import Logo from "./Logo";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Flex from "./Flex";
 | 
			
		||||
import ThemeChanger from "./ThemeChanger";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { FolderOpen, X, ArrowUpRight, BookOpen } from 'phosphor-react'
 | 
			
		||||
 | 
			
		||||
import Stack from './Stack'
 | 
			
		||||
import Logo from './Logo'
 | 
			
		||||
import Button from './Button'
 | 
			
		||||
import Flex from './Flex'
 | 
			
		||||
import Container from './Container'
 | 
			
		||||
import Box from './Box'
 | 
			
		||||
import ThemeChanger from './ThemeChanger'
 | 
			
		||||
import state from '../state'
 | 
			
		||||
import Heading from './Heading'
 | 
			
		||||
import Text from './Text'
 | 
			
		||||
import Spinner from './Spinner'
 | 
			
		||||
import truncate from '../utils/truncate'
 | 
			
		||||
import ButtonGroup from './ButtonGroup'
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogClose,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogTrigger
 | 
			
		||||
} from './Dialog'
 | 
			
		||||
import PanelBox from './PanelBox'
 | 
			
		||||
import { templateFileIds } from '../state/constants'
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const ImageWrapper = styled(Flex, {
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  mt: '$2',
 | 
			
		||||
  mb: '$10',
 | 
			
		||||
  svg: {
 | 
			
		||||
    // fill: "red",
 | 
			
		||||
    '.angle': {
 | 
			
		||||
      fill: '$text'
 | 
			
		||||
    },
 | 
			
		||||
    ':not(.angle)': {
 | 
			
		||||
      stroke: '$text'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const Navigation = () => {
 | 
			
		||||
  const { data: session, status } = useSession();
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  const slug = router.query?.slug
 | 
			
		||||
  const gistId = Array.isArray(slug) ? slug[0] : null
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      as="nav"
 | 
			
		||||
      css={{
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        height: "60px",
 | 
			
		||||
        borderBottom: "1px solid $slate6",
 | 
			
		||||
        position: "relative",
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        backgroundColor: '$mauve1',
 | 
			
		||||
        borderBottom: '1px solid $mauve6',
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        zIndex: 2003,
 | 
			
		||||
        height: '60px'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Container
 | 
			
		||||
        css={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          py: "$2",
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          alignItems: 'center'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Link href="/" passHref>
 | 
			
		||||
          <Box
 | 
			
		||||
            as="a"
 | 
			
		||||
            css={{ display: "flex", alignItems: "center", color: "$textColor" }}
 | 
			
		||||
          >
 | 
			
		||||
            <Logo width="30px" height="30px" />
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Link>
 | 
			
		||||
        <Stack css={{ ml: "$4", gap: "$3" }}>
 | 
			
		||||
          <Link href="/develop" passHref shallow>
 | 
			
		||||
            <Button
 | 
			
		||||
        <Flex
 | 
			
		||||
          css={{
 | 
			
		||||
            flex: 1,
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            borderRight: '1px solid $colors$mauve6',
 | 
			
		||||
            py: '$3',
 | 
			
		||||
            pr: '$4'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref>
 | 
			
		||||
            <Box
 | 
			
		||||
              as="a"
 | 
			
		||||
              outline={!router.pathname.includes("/develop")}
 | 
			
		||||
              uppercase
 | 
			
		||||
            >
 | 
			
		||||
              Develop
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Link>
 | 
			
		||||
          <Link href="/deploy" passHref shallow>
 | 
			
		||||
            <Button
 | 
			
		||||
              as="a"
 | 
			
		||||
              outline={!router.pathname.includes("/deploy")}
 | 
			
		||||
              uppercase
 | 
			
		||||
            >
 | 
			
		||||
              Deploy
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Link>
 | 
			
		||||
          <Link href="/test" passHref shallow>
 | 
			
		||||
            <Button
 | 
			
		||||
              as="a"
 | 
			
		||||
              outline={!router.pathname.includes("/test")}
 | 
			
		||||
              uppercase
 | 
			
		||||
            >
 | 
			
		||||
              Test
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Link>
 | 
			
		||||
        </Stack>
 | 
			
		||||
        <Stack css={{ color: "text", ml: "auto" }}>
 | 
			
		||||
          <ThemeChanger />
 | 
			
		||||
          {status === "authenticated" && (
 | 
			
		||||
            <DropdownMenu>
 | 
			
		||||
              <DropdownMenuTrigger asChild>
 | 
			
		||||
                <Box
 | 
			
		||||
                  css={{
 | 
			
		||||
                    borderRadius: "$full",
 | 
			
		||||
                    overflow: "hidden",
 | 
			
		||||
                    width: "30px",
 | 
			
		||||
                    height: "30px",
 | 
			
		||||
                    position: "relative",
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Image
 | 
			
		||||
                    src={session?.user?.image || ""}
 | 
			
		||||
                    width="30px"
 | 
			
		||||
                    height="30px"
 | 
			
		||||
                    objectFit="cover"
 | 
			
		||||
                    alt="User avatar"
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
              </DropdownMenuTrigger>
 | 
			
		||||
              <DropdownMenuContent>
 | 
			
		||||
                <DropdownMenuItem disabled onClick={() => signOut()}>
 | 
			
		||||
                  <User size="16px" /> {session?.user?.username} (
 | 
			
		||||
                  {session?.user.name})
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
                <DropdownMenuItem
 | 
			
		||||
                  onClick={() =>
 | 
			
		||||
                    window.open(
 | 
			
		||||
                      `http://gist.github.com/${session?.user.username}`
 | 
			
		||||
                    )
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                  <ArrowSquareOut size="16px" />
 | 
			
		||||
                  Go to your Gist
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
                <DropdownMenuSeparator />
 | 
			
		||||
                <DropdownMenuItem>
 | 
			
		||||
                  <Gear size="16px" /> Settings
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
                <DropdownMenuItem onClick={() => signOut()}>
 | 
			
		||||
                  <SignOut size="16px" /> Log out
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
                <DropdownMenuArrow offset={10} />
 | 
			
		||||
              </DropdownMenuContent>
 | 
			
		||||
            </DropdownMenu>
 | 
			
		||||
          )}
 | 
			
		||||
          {status === "unauthenticated" && (
 | 
			
		||||
            <Button outline onClick={() => signIn("github")}>
 | 
			
		||||
              <GithubLogo size="16px" /> Github Login
 | 
			
		||||
            </Button>
 | 
			
		||||
          )}
 | 
			
		||||
          {status === "loading" && "loading"}
 | 
			
		||||
          {/* <Box
 | 
			
		||||
              css={{
 | 
			
		||||
                border: "1px solid",
 | 
			
		||||
                borderRadius: "3px",
 | 
			
		||||
                borderColor: "text",
 | 
			
		||||
                p: 1,
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                color: '$textColor'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <BookOpen size="20px" />
 | 
			
		||||
            </Box> */}
 | 
			
		||||
        </Stack>
 | 
			
		||||
              <Logo width="32px" height="32px" />
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Link>
 | 
			
		||||
          <Flex
 | 
			
		||||
            css={{
 | 
			
		||||
              ml: '$5',
 | 
			
		||||
              flexDirection: 'column',
 | 
			
		||||
              gap: '1px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {snap.loading ? (
 | 
			
		||||
              <Spinner />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <Heading css={{ lineHeight: 1 }}>{snap.gistName || 'XRPL Hooks'}</Heading>
 | 
			
		||||
                <Text css={{ fontSize: '$xs', color: '$mauve10', lineHeight: 1 }}>
 | 
			
		||||
                  {snap.files.length > 0 ? 'Gist: ' : 'Builder'}
 | 
			
		||||
                  {snap.files.length > 0 && (
 | 
			
		||||
                    <Link
 | 
			
		||||
                      href={`https://gist.github.com/${snap.gistOwner || ''}/${snap.gistId || ''}`}
 | 
			
		||||
                      passHref
 | 
			
		||||
                    >
 | 
			
		||||
                      <Text
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                        rel="noreferrer noopener"
 | 
			
		||||
                        css={{ color: '$mauve12' }}
 | 
			
		||||
                      >
 | 
			
		||||
                        {`${snap.gistOwner || '-'}/${truncate(snap.gistId || '')}`}
 | 
			
		||||
                      </Text>
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  )}
 | 
			
		||||
                </Text>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </Flex>
 | 
			
		||||
          {router.isReady && (
 | 
			
		||||
            <ButtonGroup css={{ marginLeft: 'auto' }}>
 | 
			
		||||
              <Dialog open={snap.mainModalOpen} onOpenChange={open => (state.mainModalOpen = open)}>
 | 
			
		||||
                <DialogTrigger asChild>
 | 
			
		||||
                  <Button outline>
 | 
			
		||||
                    <FolderOpen size="15px" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </DialogTrigger>
 | 
			
		||||
                <DialogContent
 | 
			
		||||
                  css={{
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    maxWidth: '1080px',
 | 
			
		||||
                    width: '80vw',
 | 
			
		||||
                    maxHeight: '80%',
 | 
			
		||||
                    backgroundColor: '$mauve1 !important',
 | 
			
		||||
                    overflowY: 'auto',
 | 
			
		||||
                    background: 'black',
 | 
			
		||||
                    p: 0
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Flex
 | 
			
		||||
                    css={{
 | 
			
		||||
                      flexDirection: 'column',
 | 
			
		||||
                      height: '100%',
 | 
			
		||||
                      '@md': {
 | 
			
		||||
                        flexDirection: 'row',
 | 
			
		||||
                        height: '100%'
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Flex
 | 
			
		||||
                      css={{
 | 
			
		||||
                        borderBottom: '1px solid $colors$mauve5',
 | 
			
		||||
                        width: '100%',
 | 
			
		||||
                        minWidth: '240px',
 | 
			
		||||
                        flexDirection: 'column',
 | 
			
		||||
                        p: '$7',
 | 
			
		||||
                        backgroundColor: '$mauve2',
 | 
			
		||||
                        '@md': {
 | 
			
		||||
                          width: '30%',
 | 
			
		||||
                          maxWidth: '300px',
 | 
			
		||||
                          borderBottom: '0px',
 | 
			
		||||
                          borderRight: '1px solid $colors$mauve5'
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <DialogTitle
 | 
			
		||||
                        css={{
 | 
			
		||||
                          textTransform: 'uppercase',
 | 
			
		||||
                          display: 'inline-flex',
 | 
			
		||||
                          alignItems: 'center',
 | 
			
		||||
                          gap: '$3',
 | 
			
		||||
                          fontSize: '$xl',
 | 
			
		||||
                          lineHeight: '$one',
 | 
			
		||||
                          fontWeight: '$bold'
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Logo width="48px" height="48px" /> XRPL Hooks Builder
 | 
			
		||||
                      </DialogTitle>
 | 
			
		||||
                      <DialogDescription as="div">
 | 
			
		||||
                        <Text
 | 
			
		||||
                          css={{
 | 
			
		||||
                            display: 'inline-flex',
 | 
			
		||||
                            color: 'inherit',
 | 
			
		||||
                            my: '$5',
 | 
			
		||||
                            mb: '$7'
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          Hooks add smart contract functionality to the XRP Ledger.
 | 
			
		||||
                        </Text>
 | 
			
		||||
                        <Flex css={{ flexDirection: 'column', gap: '$2', mt: '$2' }}>
 | 
			
		||||
                          <Text
 | 
			
		||||
                            css={{
 | 
			
		||||
                              display: 'inline-flex',
 | 
			
		||||
                              alignItems: 'center',
 | 
			
		||||
                              gap: '$3',
 | 
			
		||||
                              color: '$purple11',
 | 
			
		||||
                              '&:hover': {
 | 
			
		||||
                                color: '$purple12'
 | 
			
		||||
                              },
 | 
			
		||||
                              '&:focus': {
 | 
			
		||||
                                outline: 0
 | 
			
		||||
                              }
 | 
			
		||||
                            }}
 | 
			
		||||
                            as="a"
 | 
			
		||||
                            rel="noreferrer noopener"
 | 
			
		||||
                            target="_blank"
 | 
			
		||||
                            href="https://github.com/XRPL-Labs/xrpld-hooks"
 | 
			
		||||
                          >
 | 
			
		||||
                            <ArrowUpRight size="15px" /> Hooks Github
 | 
			
		||||
                          </Text>
 | 
			
		||||
 | 
			
		||||
                          <Text
 | 
			
		||||
                            css={{
 | 
			
		||||
                              display: 'inline-flex',
 | 
			
		||||
                              alignItems: 'center',
 | 
			
		||||
                              gap: '$3',
 | 
			
		||||
                              color: '$purple11',
 | 
			
		||||
                              '&:hover': {
 | 
			
		||||
                                color: '$purple12'
 | 
			
		||||
                              },
 | 
			
		||||
                              '&:focus': {
 | 
			
		||||
                                outline: 0
 | 
			
		||||
                              }
 | 
			
		||||
                            }}
 | 
			
		||||
                            as="a"
 | 
			
		||||
                            rel="noreferrer noopener"
 | 
			
		||||
                            target="_blank"
 | 
			
		||||
                            href="https://xrpl-hooks.readme.io/v2.0/docs"
 | 
			
		||||
                          >
 | 
			
		||||
                            <ArrowUpRight size="15px" /> Hooks documentation
 | 
			
		||||
                          </Text>
 | 
			
		||||
                          <Text
 | 
			
		||||
                            css={{
 | 
			
		||||
                              display: 'inline-flex',
 | 
			
		||||
                              alignItems: 'center',
 | 
			
		||||
                              gap: '$3',
 | 
			
		||||
                              color: '$purple11',
 | 
			
		||||
                              '&:hover': {
 | 
			
		||||
                                color: '$purple12'
 | 
			
		||||
                              },
 | 
			
		||||
                              '&:focus': {
 | 
			
		||||
                                outline: 0
 | 
			
		||||
                              }
 | 
			
		||||
                            }}
 | 
			
		||||
                            as="a"
 | 
			
		||||
                            rel="noreferrer noopener"
 | 
			
		||||
                            target="_blank"
 | 
			
		||||
                            href="https://xrpl.org/docs.html"
 | 
			
		||||
                          >
 | 
			
		||||
                            <ArrowUpRight size="15px" /> XRPL documentation
 | 
			
		||||
                          </Text>
 | 
			
		||||
                        </Flex>
 | 
			
		||||
                      </DialogDescription>
 | 
			
		||||
                    </Flex>
 | 
			
		||||
 | 
			
		||||
                    <Flex
 | 
			
		||||
                      css={{
 | 
			
		||||
                        display: 'grid',
 | 
			
		||||
                        gridTemplateColumns: '1fr',
 | 
			
		||||
                        gridTemplateRows: 'max-content',
 | 
			
		||||
                        flex: 1,
 | 
			
		||||
                        p: '$7',
 | 
			
		||||
                        pb: '$16',
 | 
			
		||||
                        gap: '$3',
 | 
			
		||||
                        alignItems: 'normal',
 | 
			
		||||
                        flexWrap: 'wrap',
 | 
			
		||||
                        backgroundColor: '$mauve1',
 | 
			
		||||
                        '@md': {
 | 
			
		||||
                          gridTemplateColumns: '1fr 1fr',
 | 
			
		||||
                          gridTemplateRows: 'max-content'
 | 
			
		||||
                        },
 | 
			
		||||
                        '@lg': {
 | 
			
		||||
                          gridTemplateColumns: '1fr 1fr 1fr',
 | 
			
		||||
                          gridTemplateRows: 'max-content'
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {Object.values(templateFileIds).map(template => (
 | 
			
		||||
                        <PanelBox key={template.id} as="a" href={`/develop/${template.id}`}>
 | 
			
		||||
                          <ImageWrapper>{template.icon()}</ImageWrapper>
 | 
			
		||||
                          <Heading>{template.name}</Heading>
 | 
			
		||||
 | 
			
		||||
                          <Text>{template.description}</Text>
 | 
			
		||||
                        </PanelBox>
 | 
			
		||||
                      ))}
 | 
			
		||||
                    </Flex>
 | 
			
		||||
                  </Flex>
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Box
 | 
			
		||||
                      css={{
 | 
			
		||||
                        position: 'absolute',
 | 
			
		||||
                        top: '$1',
 | 
			
		||||
                        right: '$1',
 | 
			
		||||
                        cursor: 'pointer',
 | 
			
		||||
                        background: '$mauve1',
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        borderRadius: '$full',
 | 
			
		||||
                        p: '$1'
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <X size="20px" />
 | 
			
		||||
                    </Box>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                </DialogContent>
 | 
			
		||||
              </Dialog>
 | 
			
		||||
              <ThemeChanger />
 | 
			
		||||
            </ButtonGroup>
 | 
			
		||||
          )}
 | 
			
		||||
        </Flex>
 | 
			
		||||
        <Flex
 | 
			
		||||
          css={{
 | 
			
		||||
            flexWrap: 'nowrap',
 | 
			
		||||
            marginLeft: '$4',
 | 
			
		||||
            overflowX: 'scroll',
 | 
			
		||||
            '&::-webkit-scrollbar': {
 | 
			
		||||
              height: 0,
 | 
			
		||||
              background: 'transparent'
 | 
			
		||||
            },
 | 
			
		||||
            scrollbarColor: 'transparent',
 | 
			
		||||
            scrollbarWidth: 'none'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Stack
 | 
			
		||||
            css={{
 | 
			
		||||
              ml: '$4',
 | 
			
		||||
              gap: '$3',
 | 
			
		||||
              flexWrap: 'nowrap',
 | 
			
		||||
              alignItems: 'center',
 | 
			
		||||
              marginLeft: 'auto'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <ButtonGroup>
 | 
			
		||||
              <Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref shallow>
 | 
			
		||||
                <Button as="a" outline={!router.pathname.includes('/develop')} uppercase>
 | 
			
		||||
                  Develop
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Link>
 | 
			
		||||
              <Link href={gistId ? `/deploy/${gistId}` : '/deploy'} passHref shallow>
 | 
			
		||||
                <Button as="a" outline={!router.pathname.includes('/deploy')} uppercase>
 | 
			
		||||
                  Deploy
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Link>
 | 
			
		||||
              <Link href={gistId ? `/test/${gistId}` : '/test'} passHref shallow>
 | 
			
		||||
                <Button as="a" outline={!router.pathname.includes('/test')} uppercase>
 | 
			
		||||
                  Test
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Link>
 | 
			
		||||
            </ButtonGroup>
 | 
			
		||||
            <Link href="https://xrpl-hooks.readme.io/v2.0" passHref>
 | 
			
		||||
              <a target="_blank" rel="noreferrer noopener">
 | 
			
		||||
                <Button outline>
 | 
			
		||||
                  <BookOpen size="15px" />
 | 
			
		||||
                </Button>
 | 
			
		||||
              </a>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Navigation;
 | 
			
		||||
export default Navigation
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								components/PanelBox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,30 @@
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import Heading from './Heading'
 | 
			
		||||
import Text from './Text'
 | 
			
		||||
 | 
			
		||||
const PanelBox = styled('div', {
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexDirection: 'column',
 | 
			
		||||
  border: '1px solid $colors$mauve6',
 | 
			
		||||
  backgroundColor: '$mauve2',
 | 
			
		||||
  padding: '$3',
 | 
			
		||||
  borderRadius: '$sm',
 | 
			
		||||
  fontWeight: 'lighter',
 | 
			
		||||
  height: 'auto',
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
  flex: '1 1 0px',
 | 
			
		||||
  '&:hover': {
 | 
			
		||||
    border: '1px solid $colors$mauve9'
 | 
			
		||||
  },
 | 
			
		||||
  [`& ${Heading}`]: {
 | 
			
		||||
    fontWeight: 'lighter',
 | 
			
		||||
    mb: '$2'
 | 
			
		||||
  },
 | 
			
		||||
  [`& ${Text}`]: {
 | 
			
		||||
    fontWeight: 'lighter',
 | 
			
		||||
    color: '$mauve10',
 | 
			
		||||
    fontSize: '$sm'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default PanelBox
 | 
			
		||||
							
								
								
									
										102
									
								
								components/Popover.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,102 @@
 | 
			
		||||
import React, { ReactNode } from 'react'
 | 
			
		||||
import * as PopoverPrimitive from '@radix-ui/react-popover'
 | 
			
		||||
import { styled, keyframes } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const slideUpAndFade = keyframes({
 | 
			
		||||
  '0%': { opacity: 0, transform: 'translateY(2px)' },
 | 
			
		||||
  '100%': { opacity: 1, transform: 'translateY(0)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const slideRightAndFade = keyframes({
 | 
			
		||||
  '0%': { opacity: 0, transform: 'translateX(-2px)' },
 | 
			
		||||
  '100%': { opacity: 1, transform: 'translateX(0)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const slideDownAndFade = keyframes({
 | 
			
		||||
  '0%': { opacity: 0, transform: 'translateY(-2px)' },
 | 
			
		||||
  '100%': { opacity: 1, transform: 'translateY(0)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const slideLeftAndFade = keyframes({
 | 
			
		||||
  '0%': { opacity: 0, transform: 'translateX(2px)' },
 | 
			
		||||
  '100%': { opacity: 1, transform: 'translateX(0)' }
 | 
			
		||||
})
 | 
			
		||||
const StyledContent = styled(PopoverPrimitive.Content, {
 | 
			
		||||
  borderRadius: 4,
 | 
			
		||||
  padding: '$3 $3',
 | 
			
		||||
  fontSize: 12,
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: '$text',
 | 
			
		||||
  backgroundColor: '$background',
 | 
			
		||||
  boxShadow:
 | 
			
		||||
    '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
 | 
			
		||||
  '@media (prefers-reduced-motion: no-preference)': {
 | 
			
		||||
    animationDuration: '400ms',
 | 
			
		||||
    animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
 | 
			
		||||
    willChange: 'transform, opacity',
 | 
			
		||||
    '&[data-state="open"]': {
 | 
			
		||||
      '&[data-side="top"]': { animationName: slideDownAndFade },
 | 
			
		||||
      '&[data-side="right"]': { animationName: slideLeftAndFade },
 | 
			
		||||
      '&[data-side="bottom"]': { animationName: slideUpAndFade },
 | 
			
		||||
      '&[data-side="left"]': { animationName: slideRightAndFade }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    backgroundColor: '$mauve5',
 | 
			
		||||
    boxShadow: '0px 5px 38px -2px rgba(22, 23, 24, 1), 0px 10px 20px 0px rgba(22, 23, 24, 1)'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledArrow = styled(PopoverPrimitive.Arrow, {
 | 
			
		||||
  fill: '$colors$mauve2',
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    fill: '$mauve5'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledClose = styled(PopoverPrimitive.Close, {
 | 
			
		||||
  all: 'unset',
 | 
			
		||||
  fontFamily: 'inherit',
 | 
			
		||||
  borderRadius: '100%',
 | 
			
		||||
  height: 25,
 | 
			
		||||
  width: 25,
 | 
			
		||||
  display: 'inline-flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'center',
 | 
			
		||||
  color: '$text',
 | 
			
		||||
  position: 'absolute',
 | 
			
		||||
  top: 5,
 | 
			
		||||
  right: 5
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
export const PopoverRoot = PopoverPrimitive.Root
 | 
			
		||||
export const PopoverTrigger = PopoverPrimitive.Trigger
 | 
			
		||||
export const PopoverContent = StyledContent
 | 
			
		||||
export const PopoverArrow = StyledArrow
 | 
			
		||||
export const PopoverClose = StyledClose
 | 
			
		||||
 | 
			
		||||
interface IPopover {
 | 
			
		||||
  content: string | ReactNode
 | 
			
		||||
  open?: boolean
 | 
			
		||||
  defaultOpen?: boolean
 | 
			
		||||
  onOpenChange?: (open: boolean) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Popover: React.FC<IPopover & React.ComponentProps<typeof PopoverContent>> = ({
 | 
			
		||||
  children,
 | 
			
		||||
  content,
 | 
			
		||||
  open,
 | 
			
		||||
  defaultOpen = false,
 | 
			
		||||
  onOpenChange,
 | 
			
		||||
  ...rest
 | 
			
		||||
}) => (
 | 
			
		||||
  <PopoverRoot open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
 | 
			
		||||
    <PopoverTrigger asChild>{children}</PopoverTrigger>
 | 
			
		||||
    <PopoverContent sideOffset={5} {...rest}>
 | 
			
		||||
      {content} <PopoverArrow offset={5} className="arrow" />
 | 
			
		||||
    </PopoverContent>
 | 
			
		||||
  </PopoverRoot>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default Popover
 | 
			
		||||
							
								
								
									
										27
									
								
								components/Pre.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,27 @@
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const Pre = styled('span', {
 | 
			
		||||
  m: 0,
 | 
			
		||||
  wordBreak: 'break-all',
 | 
			
		||||
  fontFamily: '$monospace',
 | 
			
		||||
  whiteSpace: 'pre-wrap',
 | 
			
		||||
  variants: {
 | 
			
		||||
    fluid: {
 | 
			
		||||
      true: {
 | 
			
		||||
        width: '100%'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    line: {
 | 
			
		||||
      true: {
 | 
			
		||||
        whiteSpace: 'pre-line'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    block: {
 | 
			
		||||
      true: {
 | 
			
		||||
        display: 'block'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Pre
 | 
			
		||||
							
								
								
									
										24
									
								
								components/ResultLink.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
			
		||||
import { FC } from 'react'
 | 
			
		||||
import { Link } from '.'
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  result?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ResultLink: FC<Props> = ({ result }) => {
 | 
			
		||||
  if (!result) return null
 | 
			
		||||
  let href: string
 | 
			
		||||
  if (result === 'tesSUCCESS') {
 | 
			
		||||
    href = 'https://xrpl.org/tes-success.html'
 | 
			
		||||
  } else {
 | 
			
		||||
    // Going shortcut here because of url structure, if that changes we will do it manually
 | 
			
		||||
    href = `https://xrpl.org/${result.slice(0, 3)}-codes.html`
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Link as="a" href={href} target="_blank" rel="noopener noreferrer">
 | 
			
		||||
      {result}
 | 
			
		||||
    </Link>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ResultLink
 | 
			
		||||
							
								
								
									
										333
									
								
								components/RunScript/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,333 @@
 | 
			
		||||
import { Play, X } from 'phosphor-react'
 | 
			
		||||
import { HTMLInputTypeAttribute, useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import state, { IAccount, IFile, ILog } from '../../state'
 | 
			
		||||
import Button from '../Button'
 | 
			
		||||
import Box from '../Box'
 | 
			
		||||
import Input, { Label } from '../Input'
 | 
			
		||||
import Stack from '../Stack'
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogClose
 | 
			
		||||
} from '../Dialog'
 | 
			
		||||
import Flex from '../Flex'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import Select from '../Select'
 | 
			
		||||
import Text from '../Text'
 | 
			
		||||
import { saveFile } from '../../state/actions/saveFile'
 | 
			
		||||
import { getErrors, getTags } from '../../utils/comment-parser'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
 | 
			
		||||
const generateHtmlTemplate = async (code: string, data?: Record<string, any>) => {
 | 
			
		||||
  let processString: string | undefined
 | 
			
		||||
  const process = { env: { NODE_ENV: 'production' } } as any
 | 
			
		||||
  if (data) {
 | 
			
		||||
    Object.keys(data).forEach(key => {
 | 
			
		||||
      process.env[key] = data[key]
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  processString = JSON.stringify(process)
 | 
			
		||||
 | 
			
		||||
  const libs = (await import("xrpl-accountlib/dist/browser.hook-bundle.js")).default;
 | 
			
		||||
  return `
 | 
			
		||||
  <html>
 | 
			
		||||
  <head>
 | 
			
		||||
    <script>
 | 
			
		||||
      var log = console.log;
 | 
			
		||||
      var errorLog = console.error;
 | 
			
		||||
      var infoLog = console.info;
 | 
			
		||||
      var warnLog = console.warn
 | 
			
		||||
      console.log = function(){
 | 
			
		||||
        var args = Array.from(arguments);
 | 
			
		||||
        parent.window.postMessage({ type: 'log', args: args || [] }, '*');
 | 
			
		||||
        log.apply(console, args);
 | 
			
		||||
      }
 | 
			
		||||
      console.error = function(){
 | 
			
		||||
        var args = Array.from(arguments);
 | 
			
		||||
        parent.window.postMessage({ type: 'error', args: args || [] }, '*');
 | 
			
		||||
        errorLog.apply(console, args);
 | 
			
		||||
      }
 | 
			
		||||
      console.info = function(){
 | 
			
		||||
        var args = Array.from(arguments);
 | 
			
		||||
        parent.window.postMessage({ type: 'info', args: args || [] }, '*');
 | 
			
		||||
        infoLog.apply(console, args);
 | 
			
		||||
      }
 | 
			
		||||
      console.warn = function(){
 | 
			
		||||
        var args = Array.from(arguments);
 | 
			
		||||
        parent.window.postMessage({ type: 'warning', args: args || [] }, '*');
 | 
			
		||||
        warnLog.apply(console, args);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
     
 | 
			
		||||
      var process = '${processString || '{}'}';
 | 
			
		||||
      process = JSON.parse(process);
 | 
			
		||||
      window.process = process
 | 
			
		||||
 | 
			
		||||
      function windowErrorHandler(event) {
 | 
			
		||||
        event.preventDefault() // to prevent automatically logging to console
 | 
			
		||||
        console.error(event.error?.toString())
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      window.addEventListener('error', windowErrorHandler);
 | 
			
		||||
    </script>
 | 
			
		||||
    <script>
 | 
			
		||||
      ${libs}
 | 
			
		||||
    </script>
 | 
			
		||||
    <script type="module">
 | 
			
		||||
      ${code}
 | 
			
		||||
    </script>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
  </body>
 | 
			
		||||
  </html>
 | 
			
		||||
  `
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Fields = Record<
 | 
			
		||||
  string,
 | 
			
		||||
  {
 | 
			
		||||
    name: string
 | 
			
		||||
    value: string
 | 
			
		||||
    type?: 'Account' | `Account.${keyof IAccount}` | HTMLInputTypeAttribute
 | 
			
		||||
    description?: string
 | 
			
		||||
    required?: boolean
 | 
			
		||||
  }
 | 
			
		||||
>
 | 
			
		||||
 | 
			
		||||
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  const [templateError, setTemplateError] = useState('')
 | 
			
		||||
  const [fields, setFields] = useState<Fields>({})
 | 
			
		||||
  const [iFrameCode, setIframeCode] = useState('')
 | 
			
		||||
  const [isDialogOpen, setIsDialogOpen] = useState(false)
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const getFields = useCallback(() => {
 | 
			
		||||
    const inputTags = ['input', 'param', 'arg', 'argument']
 | 
			
		||||
    const tags = getTags(content)
 | 
			
		||||
      .filter(tag => inputTags.includes(tag.tag))
 | 
			
		||||
      .filter(tag => !!tag.name)
 | 
			
		||||
 | 
			
		||||
    let _fields = tags.map(tag => ({
 | 
			
		||||
      name: tag.name,
 | 
			
		||||
      value: tag.default || '',
 | 
			
		||||
      type: tag.type,
 | 
			
		||||
      description: tag.description,
 | 
			
		||||
      required: !tag.optional
 | 
			
		||||
    }))
 | 
			
		||||
 | 
			
		||||
    const fields: Fields = _fields.reduce((acc, field) => {
 | 
			
		||||
      acc[field.name] = field
 | 
			
		||||
      return acc
 | 
			
		||||
    }, {} as Fields)
 | 
			
		||||
 | 
			
		||||
    const error = getErrors(content)
 | 
			
		||||
    if (error) setTemplateError(error.message)
 | 
			
		||||
    else setTemplateError('')
 | 
			
		||||
 | 
			
		||||
    return fields
 | 
			
		||||
  }, [content])
 | 
			
		||||
 | 
			
		||||
  const runScript = useCallback(async () => {
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    try {
 | 
			
		||||
      // Show loading toast only after 1 second, otherwise skip it.
 | 
			
		||||
      let loaded = false
 | 
			
		||||
      let toastId: string | undefined;
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        if (!loaded) {
 | 
			
		||||
          toastId = toast.loading('Loading packages, may take a few seconds...', {
 | 
			
		||||
            position: 'bottom-center',
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      }, 1000)
 | 
			
		||||
 | 
			
		||||
      let data: any = {}
 | 
			
		||||
      Object.keys(fields).forEach(key => {
 | 
			
		||||
        data[key] = fields[key].value
 | 
			
		||||
      })
 | 
			
		||||
      const template = await generateHtmlTemplate(content, data)
 | 
			
		||||
      setIframeCode(template)
 | 
			
		||||
 | 
			
		||||
      loaded = true
 | 
			
		||||
      if (toastId) {
 | 
			
		||||
        toast.dismiss(toastId)
 | 
			
		||||
      }
 | 
			
		||||
      state.scriptLogs = [{ type: 'success', message: 'Started running...' }]
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      state.scriptLogs = [
 | 
			
		||||
        ...snap.scriptLogs,
 | 
			
		||||
        // @ts-expect-error
 | 
			
		||||
        { type: 'error', message: err?.message || 'Could not parse template' }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
    setIsLoading(false);
 | 
			
		||||
  }, [content, fields, snap.scriptLogs])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleEvent = (e: any) => {
 | 
			
		||||
      if (e.data.type === 'log' || e.data.type === 'error') {
 | 
			
		||||
        const data: ILog[] = e.data.args.map((msg: any) => ({
 | 
			
		||||
          type: e.data.type,
 | 
			
		||||
          message: typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2)
 | 
			
		||||
        }))
 | 
			
		||||
        state.scriptLogs = [...snap.scriptLogs, ...data]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    window.addEventListener('message', handleEvent)
 | 
			
		||||
    return () => window.removeEventListener('message', handleEvent)
 | 
			
		||||
  }, [snap.scriptLogs])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const defaultFields = getFields() || {}
 | 
			
		||||
    setFields(defaultFields)
 | 
			
		||||
  }, [content, setFields, getFields])
 | 
			
		||||
 | 
			
		||||
  const accOptions = snap.accounts?.map(acc => ({
 | 
			
		||||
    ...acc,
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
    value: acc.address
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  const isDisabled = Object.values(fields).some(field => field.required && !field.value)
 | 
			
		||||
 | 
			
		||||
  const handleRun = useCallback(async () => {
 | 
			
		||||
    if (isDisabled) return toast.error('Please fill in all the required fields.')
 | 
			
		||||
 | 
			
		||||
    state.scriptLogs = []
 | 
			
		||||
    await runScript();
 | 
			
		||||
    setIsDialogOpen(false)
 | 
			
		||||
  }, [isDisabled, runScript])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
 | 
			
		||||
        <DialogTrigger asChild>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="primary"
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              saveFile(false)
 | 
			
		||||
              setIframeCode('')
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Play weight="bold" size="16px" /> {name}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </DialogTrigger>
 | 
			
		||||
        <DialogContent>
 | 
			
		||||
          <DialogTitle>Run {name} script</DialogTitle>
 | 
			
		||||
          <DialogDescription>
 | 
			
		||||
            <Box>
 | 
			
		||||
              You are about to run scripts provided by the developer of the hook, make sure you
 | 
			
		||||
              trust the author before you continue.
 | 
			
		||||
            </Box>
 | 
			
		||||
            {templateError && (
 | 
			
		||||
              <Box
 | 
			
		||||
                as="span"
 | 
			
		||||
                css={{
 | 
			
		||||
                  display: 'block',
 | 
			
		||||
                  color: '$error',
 | 
			
		||||
                  mt: '$3',
 | 
			
		||||
                  whiteSpace: 'pre'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {templateError}
 | 
			
		||||
              </Box>
 | 
			
		||||
            )}
 | 
			
		||||
            {Object.keys(fields).length > 0 && (
 | 
			
		||||
              <Box css={{ mt: '$4', mb: 0 }}>
 | 
			
		||||
                Fill in the following parameters to run the script.
 | 
			
		||||
              </Box>
 | 
			
		||||
            )}
 | 
			
		||||
          </DialogDescription>
 | 
			
		||||
 | 
			
		||||
          <Stack css={{ width: '100%' }}>
 | 
			
		||||
            {Object.keys(fields).map(key => {
 | 
			
		||||
              const { name, value, type, description, required } = fields[key]
 | 
			
		||||
 | 
			
		||||
              const isAccount = type?.startsWith('Account')
 | 
			
		||||
              const isAccountSecret = type === 'Account.secret'
 | 
			
		||||
 | 
			
		||||
              const accountField = (isAccount && type?.split('.')[1]) || 'address'
 | 
			
		||||
 | 
			
		||||
              return (
 | 
			
		||||
                <Box key={name} css={{ width: '100%' }}>
 | 
			
		||||
                  <Label css={{ display: 'flex', justifyContent: 'space-between' }}>
 | 
			
		||||
                    <span>
 | 
			
		||||
                      {description || name} {required && <Text error>*</Text>}
 | 
			
		||||
                    </span>
 | 
			
		||||
                    {isAccountSecret && (
 | 
			
		||||
                      <Text error small css={{ alignSelf: 'end' }}>
 | 
			
		||||
                        can access account secret key
 | 
			
		||||
                      </Text>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </Label>
 | 
			
		||||
                  {isAccount ? (
 | 
			
		||||
                    <Select
 | 
			
		||||
                      css={{ mt: '$1' }}
 | 
			
		||||
                      options={accOptions}
 | 
			
		||||
                      onChange={(val: any) => {
 | 
			
		||||
                        setFields({
 | 
			
		||||
                          ...fields,
 | 
			
		||||
                          [key]: {
 | 
			
		||||
                            ...fields[key],
 | 
			
		||||
                            value: val[accountField]
 | 
			
		||||
                          }
 | 
			
		||||
                        })
 | 
			
		||||
                      }}
 | 
			
		||||
                      value={accOptions.find((acc: any) => acc[accountField] === value)}
 | 
			
		||||
                    />
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <Input
 | 
			
		||||
                      type={type || 'text'}
 | 
			
		||||
                      value={value}
 | 
			
		||||
                      css={{ mt: '$1' }}
 | 
			
		||||
                      onChange={e => {
 | 
			
		||||
                        setFields({
 | 
			
		||||
                          ...fields,
 | 
			
		||||
                          [key]: { ...fields[key], value: e.target.value }
 | 
			
		||||
                        })
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                </Box>
 | 
			
		||||
              )
 | 
			
		||||
            })}
 | 
			
		||||
            <Flex css={{ justifyContent: 'flex-end', width: '100%', gap: '$3' }}>
 | 
			
		||||
              <DialogClose asChild>
 | 
			
		||||
                <Button outline>Cancel</Button>
 | 
			
		||||
              </DialogClose>
 | 
			
		||||
              <Button variant="primary" isDisabled={isDisabled || isLoading} isLoading={isLoading} onClick={handleRun}>
 | 
			
		||||
                Run script
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Stack>
 | 
			
		||||
          <DialogClose asChild>
 | 
			
		||||
            <Box
 | 
			
		||||
              css={{
 | 
			
		||||
                position: 'absolute',
 | 
			
		||||
                top: '$1',
 | 
			
		||||
                right: '$1',
 | 
			
		||||
                cursor: 'pointer',
 | 
			
		||||
                background: '$mauve1',
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                borderRadius: '$full',
 | 
			
		||||
                p: '$1'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <X size="20px" />
 | 
			
		||||
            </Box>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
      {iFrameCode && (
 | 
			
		||||
        <iframe style={{ display: 'none' }} srcDoc={iFrameCode} sandbox="allow-scripts" />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default RunScript
 | 
			
		||||
							
								
								
									
										123
									
								
								components/Select.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,123 @@
 | 
			
		||||
import { forwardRef } from 'react'
 | 
			
		||||
import { mauve, mauveDark, purple, purpleDark } from '@radix-ui/colors'
 | 
			
		||||
import { useTheme } from 'next-themes'
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import dynamic from 'next/dynamic'
 | 
			
		||||
import type { Props } from 'react-select'
 | 
			
		||||
const SelectInput = dynamic(() => import('react-select'), { ssr: false })
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line react/display-name
 | 
			
		||||
const Select = forwardRef<any, Props>((props, ref) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const isDark = theme === 'dark'
 | 
			
		||||
  const colors: any = {
 | 
			
		||||
    // primary: pink.pink9,
 | 
			
		||||
    active: isDark ? purpleDark.purple9 : purple.purple9,
 | 
			
		||||
    activeLight: isDark ? purpleDark.purple5 : purple.purple5,
 | 
			
		||||
    primary: isDark ? mauveDark.mauve4 : mauve.mauve4,
 | 
			
		||||
    secondary: isDark ? mauveDark.mauve8 : mauve.mauve8,
 | 
			
		||||
    background: isDark ? mauveDark.mauve4 : mauve.mauve4,
 | 
			
		||||
    searchText: isDark ? mauveDark.mauve12 : mauve.mauve12,
 | 
			
		||||
    bg: isDark ? mauveDark.mauve1 : mauve.mauve1,
 | 
			
		||||
    dropDownBg: isDark ? mauveDark.mauve5 : mauve.mauve2,
 | 
			
		||||
    mauve4: isDark ? mauveDark.mauve4 : mauve.mauve4,
 | 
			
		||||
    mauve5: isDark ? mauveDark.mauve5 : mauve.mauve5,
 | 
			
		||||
    mauve8: isDark ? mauveDark.mauve8 : mauve.mauve8,
 | 
			
		||||
    mauve9: isDark ? mauveDark.mauve9 : mauve.mauve9,
 | 
			
		||||
    mauve12: isDark ? mauveDark.mauve12 : mauve.mauve12,
 | 
			
		||||
    border: isDark ? mauveDark.mauve10 : mauve.mauve10,
 | 
			
		||||
    placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11
 | 
			
		||||
  }
 | 
			
		||||
  colors.outline = colors.background
 | 
			
		||||
  colors.selected = colors.secondary
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectInput
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      menuPosition={props.menuPosition || 'fixed'}
 | 
			
		||||
      styles={{
 | 
			
		||||
        container: provided => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            position: 'relative',
 | 
			
		||||
            width: '100%'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        singleValue: provided => ({
 | 
			
		||||
          ...provided,
 | 
			
		||||
          color: colors.mauve12
 | 
			
		||||
        }),
 | 
			
		||||
        menu: provided => ({
 | 
			
		||||
          ...provided,
 | 
			
		||||
          backgroundColor: colors.dropDownBg
 | 
			
		||||
        }),
 | 
			
		||||
        control: (provided, state) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            minHeight: 0,
 | 
			
		||||
            border: '0px',
 | 
			
		||||
            backgroundColor: colors.mauve4,
 | 
			
		||||
            boxShadow: `0 0 0 1px ${state.isFocused ? colors.border : colors.secondary}`
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        input: provided => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            color: '$text'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        multiValue: provided => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            backgroundColor: colors.mauve8
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        multiValueLabel: provided => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            color: colors.mauve12
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        multiValueRemove: provided => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            ':hover': {
 | 
			
		||||
              background: colors.mauve9
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        option: (provided, state) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            color: colors.searchText,
 | 
			
		||||
            backgroundColor: state.isFocused ? colors.activeLight : colors.dropDownBg,
 | 
			
		||||
            ':hover': {
 | 
			
		||||
              backgroundColor: colors.active,
 | 
			
		||||
              color: '#ffffff'
 | 
			
		||||
            },
 | 
			
		||||
            ':selected': {
 | 
			
		||||
              backgroundColor: 'red'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        indicatorSeparator: provided => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            backgroundColor: colors.secondary
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        dropdownIndicator: (provided, state) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            color: state.isFocused ? colors.border : colors.secondary,
 | 
			
		||||
            ':hover': {
 | 
			
		||||
              color: colors.border
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default styled(Select, {})
 | 
			
		||||
							
								
								
									
										71
									
								
								components/Sequence.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,71 @@
 | 
			
		||||
import { FC, useCallback, useState } from 'react'
 | 
			
		||||
import state from '../state'
 | 
			
		||||
import { Flex, Input, Button } from '.'
 | 
			
		||||
import fetchAccountInfo from '../utils/accountInfo'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
 | 
			
		||||
interface AccountSequenceProps {
 | 
			
		||||
  address?: string
 | 
			
		||||
}
 | 
			
		||||
const AccountSequence: FC<AccountSequenceProps> = ({ address }) => {
 | 
			
		||||
  const { accounts } = useSnapshot(state)
 | 
			
		||||
  const account = accounts.find(acc => acc.address === address)
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false)
 | 
			
		||||
  const setSequence = useCallback(
 | 
			
		||||
    (sequence: number) => {
 | 
			
		||||
      const acc = state.accounts.find(acc => acc.address == address)
 | 
			
		||||
      if (!acc) return
 | 
			
		||||
      acc.sequence = sequence
 | 
			
		||||
    },
 | 
			
		||||
    [address]
 | 
			
		||||
  )
 | 
			
		||||
  const handleUpdateSequence = useCallback(
 | 
			
		||||
    async (silent?: boolean) => {
 | 
			
		||||
      if (!account) return
 | 
			
		||||
      setIsLoading(true)
 | 
			
		||||
 | 
			
		||||
      const info = await fetchAccountInfo(account.address, { silent })
 | 
			
		||||
      if (info) {
 | 
			
		||||
        setSequence(info.Sequence)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setIsLoading(false)
 | 
			
		||||
    },
 | 
			
		||||
    [account, setSequence]
 | 
			
		||||
  )
 | 
			
		||||
  const disabled = !account
 | 
			
		||||
  return (
 | 
			
		||||
    <Flex row align="center" fluid>
 | 
			
		||||
      <Input
 | 
			
		||||
        placeholder="Account sequence"
 | 
			
		||||
        value={account?.sequence}
 | 
			
		||||
        disabled={!account}
 | 
			
		||||
        type="number"
 | 
			
		||||
        readOnly={true}
 | 
			
		||||
      />
 | 
			
		||||
      <Button
 | 
			
		||||
        size="xs"
 | 
			
		||||
        variant="primary"
 | 
			
		||||
        type="button"
 | 
			
		||||
        outline
 | 
			
		||||
        disabled={disabled}
 | 
			
		||||
        isDisabled={disabled}
 | 
			
		||||
        isLoading={isLoading}
 | 
			
		||||
        css={{
 | 
			
		||||
          background: '$backgroundAlt',
 | 
			
		||||
          position: 'absolute',
 | 
			
		||||
          right: '$2',
 | 
			
		||||
          fontSize: '$xs',
 | 
			
		||||
          cursor: 'pointer',
 | 
			
		||||
          alignContent: 'center',
 | 
			
		||||
          display: 'flex'
 | 
			
		||||
        }}
 | 
			
		||||
        onClick={() => handleUpdateSequence()}
 | 
			
		||||
      >
 | 
			
		||||
        Update
 | 
			
		||||
      </Button>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AccountSequence
 | 
			
		||||
							
								
								
									
										425
									
								
								components/SetHookDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,425 @@
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { Plus, Trash, X } from 'phosphor-react'
 | 
			
		||||
import { Button, Box, Text } from '.'
 | 
			
		||||
import { Stack, Flex, Select } from '.'
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogClose,
 | 
			
		||||
  DialogTrigger
 | 
			
		||||
} from './Dialog'
 | 
			
		||||
import { Input, Label } from './Input'
 | 
			
		||||
import { Controller, SubmitHandler, useFieldArray, useForm } from 'react-hook-form'
 | 
			
		||||
 | 
			
		||||
import { deployHook } from '../state/actions'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import state, { IFile, SelectOption } from '../state'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import { prepareDeployHookTx, sha256 } from '../state/actions/deployHook'
 | 
			
		||||
import estimateFee from '../utils/estimateFee'
 | 
			
		||||
import { getParameters, getInvokeOptions, transactionOptions, SetHookData } from '../utils/setHook'
 | 
			
		||||
import { capitalize } from '../utils/helpers'
 | 
			
		||||
import AccountSequence from './Sequence'
 | 
			
		||||
 | 
			
		||||
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
 | 
			
		||||
  ({ accountAddress }) => {
 | 
			
		||||
    const snap = useSnapshot(state)
 | 
			
		||||
 | 
			
		||||
    const [estimateLoading, setEstimateLoading] = useState(false)
 | 
			
		||||
    const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false)
 | 
			
		||||
 | 
			
		||||
    const compiledFiles = snap.files.filter(file => file.compiledContent)
 | 
			
		||||
    const activeFile = compiledFiles[snap.activeWat] as IFile | undefined
 | 
			
		||||
 | 
			
		||||
    const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
 | 
			
		||||
      label: acc.name,
 | 
			
		||||
      value: acc.address
 | 
			
		||||
    }))
 | 
			
		||||
 | 
			
		||||
    const [selectedAccount, setSelectedAccount] = useState(
 | 
			
		||||
      accountOptions.find(acc => acc.value === accountAddress)
 | 
			
		||||
    )
 | 
			
		||||
    const account = snap.accounts.find(acc => acc.address === selectedAccount?.value)
 | 
			
		||||
 | 
			
		||||
    const getHookNamespace = useCallback(
 | 
			
		||||
      () =>
 | 
			
		||||
        (activeFile && snap.deployValues[activeFile.name]?.HookNamespace) ||
 | 
			
		||||
        activeFile?.name.split('.')[0] ||
 | 
			
		||||
        '',
 | 
			
		||||
      [activeFile, snap.deployValues]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const getDefaultValues = useCallback((): Partial<SetHookData> => {
 | 
			
		||||
      const content = activeFile?.compiledValueSnapshot
 | 
			
		||||
      return (
 | 
			
		||||
        (activeFile && snap.deployValues[activeFile.name]) || {
 | 
			
		||||
          HookNamespace: getHookNamespace(),
 | 
			
		||||
          Invoke: getInvokeOptions(content),
 | 
			
		||||
          HookParameters: getParameters(content)
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
    }, [activeFile, getHookNamespace, snap.deployValues])
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
      register,
 | 
			
		||||
      handleSubmit,
 | 
			
		||||
      control,
 | 
			
		||||
      watch,
 | 
			
		||||
      setValue,
 | 
			
		||||
      getValues,
 | 
			
		||||
      reset,
 | 
			
		||||
      formState: { errors }
 | 
			
		||||
    } = useForm<SetHookData>({
 | 
			
		||||
      defaultValues: getDefaultValues()
 | 
			
		||||
    })
 | 
			
		||||
    const { fields, append, remove } = useFieldArray({
 | 
			
		||||
      control,
 | 
			
		||||
      name: 'HookParameters' // unique name for your Field Array
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const watchedFee = watch('Fee')
 | 
			
		||||
 | 
			
		||||
    // Reset form if activeFile changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      if (!activeFile) return
 | 
			
		||||
      const defaultValues = getDefaultValues()
 | 
			
		||||
 | 
			
		||||
      reset(defaultValues)
 | 
			
		||||
    }, [activeFile, getDefaultValues, reset])
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      if (watchedFee && (watchedFee.includes('.') || watchedFee.includes(','))) {
 | 
			
		||||
        setValue('Fee', watchedFee.replaceAll('.', '').replaceAll(',', ''))
 | 
			
		||||
      }
 | 
			
		||||
    }, [watchedFee, setValue])
 | 
			
		||||
    // const {
 | 
			
		||||
    //   fields: grantFields,
 | 
			
		||||
    //   append: grantAppend,
 | 
			
		||||
    //   remove: grantRemove,
 | 
			
		||||
    // } = useFieldArray({
 | 
			
		||||
    //   control,
 | 
			
		||||
    //   name: "HookGrants", // unique name for your Field Array
 | 
			
		||||
    // });
 | 
			
		||||
    const [hashedNamespace, setHashedNamespace] = useState('')
 | 
			
		||||
 | 
			
		||||
    const namespace = watch('HookNamespace', getHookNamespace())
 | 
			
		||||
 | 
			
		||||
    const calculateHashedValue = useCallback(async () => {
 | 
			
		||||
      const hashedVal = await sha256(namespace)
 | 
			
		||||
      setHashedNamespace(hashedVal.toUpperCase())
 | 
			
		||||
    }, [namespace])
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      calculateHashedValue()
 | 
			
		||||
    }, [namespace, calculateHashedValue])
 | 
			
		||||
 | 
			
		||||
    const calculateFee = useCallback(async () => {
 | 
			
		||||
      if (!account) return
 | 
			
		||||
 | 
			
		||||
      const formValues = getValues()
 | 
			
		||||
      const tx = await prepareDeployHookTx(account, formValues)
 | 
			
		||||
      if (!tx) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const res = await estimateFee(tx, account)
 | 
			
		||||
      if (res && res.base_fee) {
 | 
			
		||||
        setValue('Fee', Math.round(Number(res.base_fee || '')).toString())
 | 
			
		||||
      }
 | 
			
		||||
    }, [account, getValues, setValue])
 | 
			
		||||
 | 
			
		||||
    const tooLargeFile = () => {
 | 
			
		||||
      return Boolean(
 | 
			
		||||
        activeFile?.compiledContent?.byteLength && activeFile?.compiledContent?.byteLength >= 64000
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onSubmit: SubmitHandler<SetHookData> = async data => {
 | 
			
		||||
      const currAccount = state.accounts.find(acc => acc.address === account?.address)
 | 
			
		||||
      if (!account) return
 | 
			
		||||
      if (currAccount) currAccount.isLoading = true
 | 
			
		||||
 | 
			
		||||
      data.HookParameters.forEach(param => {
 | 
			
		||||
        delete param.$metaData
 | 
			
		||||
        return param
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const res = await deployHook(account, data)
 | 
			
		||||
      if (currAccount) currAccount.isLoading = false
 | 
			
		||||
 | 
			
		||||
      if (res && res.engine_result === 'tesSUCCESS') {
 | 
			
		||||
        toast.success('Transaction succeeded!')
 | 
			
		||||
        return setIsSetHookDialogOpen(false)
 | 
			
		||||
      }
 | 
			
		||||
      toast.error(`Transaction failed! (${res?.engine_result_message})`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onOpenChange = useCallback(
 | 
			
		||||
      (open: boolean) => {
 | 
			
		||||
        setIsSetHookDialogOpen(open)
 | 
			
		||||
 | 
			
		||||
        if (open) calculateFee()
 | 
			
		||||
      },
 | 
			
		||||
      [calculateFee]
 | 
			
		||||
    )
 | 
			
		||||
    return (
 | 
			
		||||
      <Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
 | 
			
		||||
        <DialogTrigger asChild>
 | 
			
		||||
          <Button
 | 
			
		||||
            ghost
 | 
			
		||||
            size="xs"
 | 
			
		||||
            uppercase
 | 
			
		||||
            variant={'secondary'}
 | 
			
		||||
            disabled={!account || account.isLoading || !activeFile || tooLargeFile()}
 | 
			
		||||
          >
 | 
			
		||||
            Set Hook
 | 
			
		||||
          </Button>
 | 
			
		||||
        </DialogTrigger>
 | 
			
		||||
        <DialogContent>
 | 
			
		||||
          <form onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
            <DialogTitle>Deploy configuration</DialogTitle>
 | 
			
		||||
            <DialogDescription as="div">
 | 
			
		||||
              <Stack css={{ width: '100%', flex: 1 }}>
 | 
			
		||||
                <Box css={{ width: '100%' }}>
 | 
			
		||||
                  <Label>Account</Label>
 | 
			
		||||
                  <Select
 | 
			
		||||
                    instanceId="deploy-account"
 | 
			
		||||
                    placeholder="Select account"
 | 
			
		||||
                    options={accountOptions}
 | 
			
		||||
                    value={selectedAccount}
 | 
			
		||||
                    onChange={(acc: any) => setSelectedAccount(acc)}
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box css={{ width: '100%', position: 'relative' }}>
 | 
			
		||||
                  <Label>Sequence</Label>
 | 
			
		||||
                  <AccountSequence address={selectedAccount?.value} />
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box css={{ width: '100%' }}>
 | 
			
		||||
                  <Label>Invoke on transactions</Label>
 | 
			
		||||
                  <Controller
 | 
			
		||||
                    name="Invoke"
 | 
			
		||||
                    control={control}
 | 
			
		||||
                    render={({ field }) => (
 | 
			
		||||
                      <Select
 | 
			
		||||
                        {...field}
 | 
			
		||||
                        closeMenuOnSelect={false}
 | 
			
		||||
                        isMulti
 | 
			
		||||
                        menuPosition="fixed"
 | 
			
		||||
                        options={transactionOptions}
 | 
			
		||||
                      />
 | 
			
		||||
                    )}
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box css={{ width: '100%' }}>
 | 
			
		||||
                  <Label>Hook Namespace Seed</Label>
 | 
			
		||||
                  <Input {...register('HookNamespace', { required: true })} autoComplete={'off'} />
 | 
			
		||||
                  {errors.HookNamespace?.type === 'required' && (
 | 
			
		||||
                    <Box css={{ display: 'inline', color: '$red11' }}>Namespace is required</Box>
 | 
			
		||||
                  )}
 | 
			
		||||
                  <Box css={{ mt: '$3' }}>
 | 
			
		||||
                    <Label>Hook Namespace (sha256)</Label>
 | 
			
		||||
                    <Input readOnly value={hashedNamespace} />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </Box>
 | 
			
		||||
 | 
			
		||||
                <Box css={{ width: '100%' }}>
 | 
			
		||||
                  <Label style={{ marginBottom: '10px', display: 'block' }}>Hook parameters</Label>
 | 
			
		||||
                  <Stack>
 | 
			
		||||
                    {fields.map((field, index) => (
 | 
			
		||||
                      <Stack key={field.id}>
 | 
			
		||||
                        <Flex column>
 | 
			
		||||
                          <Flex row>
 | 
			
		||||
                            <Input
 | 
			
		||||
                              // important to include key with field's id
 | 
			
		||||
                              placeholder="Parameter name"
 | 
			
		||||
                              readOnly={field.$metaData?.required}
 | 
			
		||||
                              {...register(
 | 
			
		||||
                                `HookParameters.${index}.HookParameter.HookParameterName`
 | 
			
		||||
                              )}
 | 
			
		||||
                            />
 | 
			
		||||
                            <Input
 | 
			
		||||
                              css={{ mx: '$2' }}
 | 
			
		||||
                              placeholder="Value (hex-quoted)"
 | 
			
		||||
                              {...register(
 | 
			
		||||
                                `HookParameters.${index}.HookParameter.HookParameterValue`,
 | 
			
		||||
                                { required: field.$metaData?.required }
 | 
			
		||||
                              )}
 | 
			
		||||
                            />
 | 
			
		||||
                            <Button onClick={() => remove(index)} variant="destroy">
 | 
			
		||||
                              <Trash weight="regular" size="16px" />
 | 
			
		||||
                            </Button>
 | 
			
		||||
                          </Flex>
 | 
			
		||||
                          {errors.HookParameters?.[index]?.HookParameter?.HookParameterValue
 | 
			
		||||
                            ?.type === 'required' && <Text error>This field is required</Text>}
 | 
			
		||||
                          <Label css={{ fontSize: '$sm', mt: '$1' }}>
 | 
			
		||||
                            {capitalize(field.$metaData?.description)}
 | 
			
		||||
                          </Label>
 | 
			
		||||
                        </Flex>
 | 
			
		||||
                      </Stack>
 | 
			
		||||
                    ))}
 | 
			
		||||
                    <Button
 | 
			
		||||
                      outline
 | 
			
		||||
                      fullWidth
 | 
			
		||||
                      type="button"
 | 
			
		||||
                      onClick={() =>
 | 
			
		||||
                        append({
 | 
			
		||||
                          HookParameter: {
 | 
			
		||||
                            HookParameterName: '',
 | 
			
		||||
                            HookParameterValue: ''
 | 
			
		||||
                          }
 | 
			
		||||
                        })
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      <Plus size="16px" />
 | 
			
		||||
                      Add Hook Parameter
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </Stack>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box css={{ width: '100%', position: 'relative' }}>
 | 
			
		||||
                  <Label>Fee</Label>
 | 
			
		||||
                  <Box css={{ display: 'flex', alignItems: 'center' }}>
 | 
			
		||||
                    <Input
 | 
			
		||||
                      type="number"
 | 
			
		||||
                      {...register('Fee', { required: true })}
 | 
			
		||||
                      autoComplete={'off'}
 | 
			
		||||
                      onKeyPress={e => {
 | 
			
		||||
                        if (e.key === '.' || e.key === ',') {
 | 
			
		||||
                          e.preventDefault()
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                      step="1"
 | 
			
		||||
                      defaultValue={10000}
 | 
			
		||||
                      css={{
 | 
			
		||||
                        '-moz-appearance': 'textfield',
 | 
			
		||||
                        '&::-webkit-outer-spin-button': {
 | 
			
		||||
                          '-webkit-appearance': 'none',
 | 
			
		||||
                          margin: 0
 | 
			
		||||
                        },
 | 
			
		||||
                        '&::-webkit-inner-spin-button ': {
 | 
			
		||||
                          '-webkit-appearance': 'none',
 | 
			
		||||
                          margin: 0
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Button
 | 
			
		||||
                      size="xs"
 | 
			
		||||
                      variant="primary"
 | 
			
		||||
                      outline
 | 
			
		||||
                      isLoading={estimateLoading}
 | 
			
		||||
                      css={{
 | 
			
		||||
                        position: 'absolute',
 | 
			
		||||
                        right: '$2',
 | 
			
		||||
                        fontSize: '$xs',
 | 
			
		||||
                        cursor: 'pointer',
 | 
			
		||||
                        alignContent: 'center',
 | 
			
		||||
                        display: 'flex'
 | 
			
		||||
                      }}
 | 
			
		||||
                      onClick={async e => {
 | 
			
		||||
                        e.preventDefault()
 | 
			
		||||
                        if (!account) return
 | 
			
		||||
                        setEstimateLoading(true)
 | 
			
		||||
                        const formValues = getValues()
 | 
			
		||||
                        try {
 | 
			
		||||
                          const tx = await prepareDeployHookTx(account, formValues)
 | 
			
		||||
                          if (tx) {
 | 
			
		||||
                            const res = await estimateFee(tx, account)
 | 
			
		||||
 | 
			
		||||
                            if (res && res.base_fee) {
 | 
			
		||||
                              setValue('Fee', Math.round(Number(res.base_fee || '')).toString())
 | 
			
		||||
                            }
 | 
			
		||||
                          }
 | 
			
		||||
                        } catch (err) {}
 | 
			
		||||
 | 
			
		||||
                        setEstimateLoading(false)
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      Suggest
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </Box>
 | 
			
		||||
                  {errors.Fee?.type === 'required' && (
 | 
			
		||||
                    <Box css={{ display: 'inline', color: '$red11' }}>Fee is required</Box>
 | 
			
		||||
                  )}
 | 
			
		||||
                </Box>
 | 
			
		||||
                {/* <Box css={{ width: "100%" }}>
 | 
			
		||||
                <label style={{ marginBottom: "10px", display: "block" }}>
 | 
			
		||||
                  Hook Grants
 | 
			
		||||
                </label>
 | 
			
		||||
                <Stack>
 | 
			
		||||
                  {grantFields.map((field, index) => (
 | 
			
		||||
                    <Stack key={field.id}>
 | 
			
		||||
                      <Input
 | 
			
		||||
                        // important to include key with field's id
 | 
			
		||||
                        placeholder="Authorize"
 | 
			
		||||
                        {...register(
 | 
			
		||||
                          `HookGrants.${index}.HookGrant.Authorize`,
 | 
			
		||||
                          { minLength: 5 }
 | 
			
		||||
                        )}
 | 
			
		||||
                      />
 | 
			
		||||
                      <Input
 | 
			
		||||
                        placeholder="HookHash"
 | 
			
		||||
                        {...register(`HookGrants.${index}.HookGrant.HookHash`, {
 | 
			
		||||
                          minLength: 64,
 | 
			
		||||
                          maxLength: 64,
 | 
			
		||||
                        })}
 | 
			
		||||
                      />
 | 
			
		||||
                      <Button
 | 
			
		||||
                        onClick={() => grantRemove(index)}
 | 
			
		||||
                        variant="destroy"
 | 
			
		||||
                      >
 | 
			
		||||
                        <Trash weight="regular" size="16px" />
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    </Stack>
 | 
			
		||||
                  ))}
 | 
			
		||||
                  <Button
 | 
			
		||||
                    outline
 | 
			
		||||
                    fullWidth
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    onClick={() =>
 | 
			
		||||
                      grantAppend({
 | 
			
		||||
                        HookGrant: {
 | 
			
		||||
                          Authorize: "",
 | 
			
		||||
                          HookHash: "",
 | 
			
		||||
                        },
 | 
			
		||||
                      })
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <Plus size="16px" />
 | 
			
		||||
                    Add Hook Grant
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Stack>
 | 
			
		||||
              </Box> */}
 | 
			
		||||
              </Stack>
 | 
			
		||||
            </DialogDescription>
 | 
			
		||||
 | 
			
		||||
            <Flex
 | 
			
		||||
              css={{
 | 
			
		||||
                marginTop: 25,
 | 
			
		||||
                justifyContent: 'flex-end',
 | 
			
		||||
                gap: '$3'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <DialogClose asChild>
 | 
			
		||||
                <Button outline>Cancel</Button>
 | 
			
		||||
              </DialogClose>
 | 
			
		||||
              {/* <DialogClose asChild> */}
 | 
			
		||||
              <Button variant="primary" type="submit" isLoading={account?.isLoading}>
 | 
			
		||||
                Set Hook
 | 
			
		||||
              </Button>
 | 
			
		||||
              {/* </DialogClose> */}
 | 
			
		||||
            </Flex>
 | 
			
		||||
            <DialogClose asChild>
 | 
			
		||||
              <Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
 | 
			
		||||
                <X size="20px" />
 | 
			
		||||
              </Box>
 | 
			
		||||
            </DialogClose>
 | 
			
		||||
          </form>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SetHookDialog.displayName = 'SetHookDialog'
 | 
			
		||||
 | 
			
		||||
export default SetHookDialog
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
import { Spinner as SpinnerIcon } from "phosphor-react";
 | 
			
		||||
import { styled, keyframes } from "../stitches.config";
 | 
			
		||||
import { Spinner as SpinnerIcon } from 'phosphor-react'
 | 
			
		||||
import { styled, keyframes } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const rotate = keyframes({
 | 
			
		||||
  "0%": { transform: "rotate(0deg)" },
 | 
			
		||||
  "100%": { transform: "rotate(360deg)" },
 | 
			
		||||
});
 | 
			
		||||
  '0%': { transform: 'rotate(0deg)' },
 | 
			
		||||
  '100%': { transform: 'rotate(-360deg)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const Spinner = styled(SpinnerIcon, {
 | 
			
		||||
  animation: `${rotate} 150ms cubic-bezier(0.16, 1, 0.3, 1) infinite`,
 | 
			
		||||
});
 | 
			
		||||
  animation: `${rotate} 150ms linear infinite`,
 | 
			
		||||
  fontSize: '16px'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Spinner;
 | 
			
		||||
export default Spinner
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,11 @@
 | 
			
		||||
import { Children } from "react";
 | 
			
		||||
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import type * as Stitches from "@stitches/react";
 | 
			
		||||
import Box from './Box'
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const StackComponent = styled(Box, {
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexWrap: "wrap",
 | 
			
		||||
  flexDirection: "row",
 | 
			
		||||
  gap: "$4",
 | 
			
		||||
});
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexWrap: 'wrap',
 | 
			
		||||
  flexDirection: 'row',
 | 
			
		||||
  gap: '$4'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default StackComponent;
 | 
			
		||||
export default StackComponent
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								components/Switch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import * as SwitchPrimitive from '@radix-ui/react-switch'
 | 
			
		||||
 | 
			
		||||
const StyledSwitch = styled(SwitchPrimitive.Root, {
 | 
			
		||||
  all: 'unset',
 | 
			
		||||
  width: 42,
 | 
			
		||||
  height: 25,
 | 
			
		||||
  backgroundColor: '$mauve9',
 | 
			
		||||
  borderRadius: '9999px',
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  boxShadow: `0 2px 10px $colors$mauve2`,
 | 
			
		||||
  WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)',
 | 
			
		||||
  '&:focus': { boxShadow: `0 0 0 2px $colors$mauveA2` },
 | 
			
		||||
  '&[data-state="checked"]': { backgroundColor: '$green11' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledThumb = styled(SwitchPrimitive.Thumb, {
 | 
			
		||||
  display: 'block',
 | 
			
		||||
  width: 21,
 | 
			
		||||
  height: 21,
 | 
			
		||||
  backgroundColor: 'white',
 | 
			
		||||
  borderRadius: '9999px',
 | 
			
		||||
  boxShadow: `0 2px 2px $colors$mauveA6`,
 | 
			
		||||
  transition: 'transform 100ms',
 | 
			
		||||
  transform: 'translateX(2px)',
 | 
			
		||||
  willChange: 'transform',
 | 
			
		||||
  '&[data-state="checked"]': { transform: 'translateX(19px)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
export const Switch = StyledSwitch
 | 
			
		||||
export const SwitchThumb = StyledThumb
 | 
			
		||||
							
								
								
									
										374
									
								
								components/Tabs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,374 @@
 | 
			
		||||
import React, { useEffect, useState, Fragment, isValidElement, useCallback } from 'react'
 | 
			
		||||
import type { ReactNode, ReactElement } from 'react'
 | 
			
		||||
import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from '.'
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogClose
 | 
			
		||||
} from './Dialog'
 | 
			
		||||
import { Plus, X } from 'phosphor-react'
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import { capitalize, getFileExtention } from '../utils/helpers'
 | 
			
		||||
import ContextMenu, { ContentMenuOption } from './ContextMenu'
 | 
			
		||||
 | 
			
		||||
const ErrorText = styled(Text, {
 | 
			
		||||
  color: '$error',
 | 
			
		||||
  mt: '$1',
 | 
			
		||||
  display: 'block'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
type Nullable<T> = T | null | undefined | false
 | 
			
		||||
 | 
			
		||||
interface TabProps {
 | 
			
		||||
  header: string
 | 
			
		||||
  children?: ReactNode
 | 
			
		||||
  renameDisabled?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO customize messages shown
 | 
			
		||||
interface Props {
 | 
			
		||||
  label?: string
 | 
			
		||||
  activeIndex?: number
 | 
			
		||||
  activeHeader?: string
 | 
			
		||||
  headless?: boolean
 | 
			
		||||
  children: ReactElement<TabProps>[]
 | 
			
		||||
  keepAllAlive?: boolean
 | 
			
		||||
  defaultExtension?: string
 | 
			
		||||
  extensionRequired?: boolean
 | 
			
		||||
  allowedExtensions?: string[]
 | 
			
		||||
  headerExtraValidation?: {
 | 
			
		||||
    regex: string | RegExp
 | 
			
		||||
    error: string
 | 
			
		||||
  }
 | 
			
		||||
  onCreateNewTab?: (name: string) => any
 | 
			
		||||
  onRenameTab?: (index: number, nwName: string, oldName?: string) => any
 | 
			
		||||
  onCloseTab?: (index: number, header?: string) => any
 | 
			
		||||
  onChangeActive?: (index: number, header?: string) => any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Tab = (props: TabProps) => null
 | 
			
		||||
 | 
			
		||||
export const Tabs = ({
 | 
			
		||||
  label = 'Tab',
 | 
			
		||||
  children,
 | 
			
		||||
  activeIndex,
 | 
			
		||||
  activeHeader,
 | 
			
		||||
  headless,
 | 
			
		||||
  keepAllAlive = false,
 | 
			
		||||
  onCreateNewTab,
 | 
			
		||||
  onCloseTab,
 | 
			
		||||
  onChangeActive,
 | 
			
		||||
  onRenameTab,
 | 
			
		||||
  headerExtraValidation,
 | 
			
		||||
  extensionRequired,
 | 
			
		||||
  defaultExtension = '',
 | 
			
		||||
  allowedExtensions
 | 
			
		||||
}: Props) => {
 | 
			
		||||
  const [active, setActive] = useState(activeIndex || 0)
 | 
			
		||||
  const tabs: TabProps[] = children.map(elem => elem.props)
 | 
			
		||||
 | 
			
		||||
  const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false)
 | 
			
		||||
  const [renamingTab, setRenamingTab] = useState<number | null>(null)
 | 
			
		||||
  const [tabname, setTabname] = useState('')
 | 
			
		||||
  const [tabnameError, setTabnameError] = useState<string | null>(null)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (activeIndex) setActive(activeIndex)
 | 
			
		||||
  }, [activeIndex])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (activeHeader) {
 | 
			
		||||
      const idx = tabs.findIndex(tab => tab.header === activeHeader)
 | 
			
		||||
      if (idx !== -1) setActive(idx)
 | 
			
		||||
      else setActive(0)
 | 
			
		||||
    }
 | 
			
		||||
  }, [activeHeader, tabs])
 | 
			
		||||
 | 
			
		||||
  // when filename changes, reset error
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setTabnameError(null)
 | 
			
		||||
  }, [tabname, setTabnameError])
 | 
			
		||||
 | 
			
		||||
  const validateTabname = useCallback(
 | 
			
		||||
    (tabname: string): { error?: string; result?: string } => {
 | 
			
		||||
      if (!tabname) {
 | 
			
		||||
        return { error: `Please enter ${label.toLocaleLowerCase()} name.` }
 | 
			
		||||
      }
 | 
			
		||||
      let ext = getFileExtention(tabname)
 | 
			
		||||
 | 
			
		||||
      if (!ext && defaultExtension) {
 | 
			
		||||
        ext = defaultExtension
 | 
			
		||||
        tabname = `${tabname}.${defaultExtension}`
 | 
			
		||||
      }
 | 
			
		||||
      if (tabs.find(tab => tab.header === tabname)) {
 | 
			
		||||
        return { error: `${capitalize(label)} name already exists.` }
 | 
			
		||||
      }
 | 
			
		||||
      if (extensionRequired && !ext) {
 | 
			
		||||
        return { error: 'File extension is required!' }
 | 
			
		||||
      }
 | 
			
		||||
      if (allowedExtensions && ext && !allowedExtensions.includes(ext)) {
 | 
			
		||||
        return { error: 'This file extension is not allowed!' }
 | 
			
		||||
      }
 | 
			
		||||
      if (headerExtraValidation && !tabname.match(headerExtraValidation.regex)) {
 | 
			
		||||
        return { error: headerExtraValidation.error }
 | 
			
		||||
      }
 | 
			
		||||
      return { result: tabname }
 | 
			
		||||
    },
 | 
			
		||||
    [allowedExtensions, defaultExtension, extensionRequired, headerExtraValidation, label, tabs]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const handleActiveChange = useCallback(
 | 
			
		||||
    (idx: number, header?: string) => {
 | 
			
		||||
      setActive(idx)
 | 
			
		||||
      onChangeActive?.(idx, header)
 | 
			
		||||
    },
 | 
			
		||||
    [onChangeActive]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const handleRenameTab = useCallback(() => {
 | 
			
		||||
    if (renamingTab === null) return
 | 
			
		||||
 | 
			
		||||
    const res = validateTabname(tabname)
 | 
			
		||||
    if (res.error) {
 | 
			
		||||
      setTabnameError(`Error: ${res.error}`)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { result: nwName = tabname } = res
 | 
			
		||||
 | 
			
		||||
    setRenamingTab(null)
 | 
			
		||||
    setTabname('')
 | 
			
		||||
 | 
			
		||||
    const oldName = tabs[renamingTab]?.header
 | 
			
		||||
    onRenameTab?.(renamingTab, nwName, oldName)
 | 
			
		||||
 | 
			
		||||
    handleActiveChange(renamingTab, nwName)
 | 
			
		||||
  }, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname])
 | 
			
		||||
 | 
			
		||||
  const handleCreateTab = useCallback(() => {
 | 
			
		||||
    const res = validateTabname(tabname)
 | 
			
		||||
    if (res.error) {
 | 
			
		||||
      setTabnameError(`Error: ${res.error}`)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const { result: _tabname = tabname } = res
 | 
			
		||||
 | 
			
		||||
    setIsNewtabDialogOpen(false)
 | 
			
		||||
    setTabname('')
 | 
			
		||||
 | 
			
		||||
    onCreateNewTab?.(_tabname)
 | 
			
		||||
 | 
			
		||||
    handleActiveChange(tabs.length, _tabname)
 | 
			
		||||
  }, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length])
 | 
			
		||||
 | 
			
		||||
  const handleCloseTab = useCallback(
 | 
			
		||||
    (idx: number) => {
 | 
			
		||||
      onCloseTab?.(idx, tabs[idx].header)
 | 
			
		||||
 | 
			
		||||
      if (idx <= active && active !== 0) {
 | 
			
		||||
        const nwActive = active - 1
 | 
			
		||||
        handleActiveChange(nwActive, tabs[nwActive].header)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [active, handleActiveChange, onCloseTab, tabs]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const closeOption = (idx: number): Nullable<ContentMenuOption> =>
 | 
			
		||||
    onCloseTab && {
 | 
			
		||||
      type: 'text',
 | 
			
		||||
      label: 'Close',
 | 
			
		||||
      key: 'close',
 | 
			
		||||
      onSelect: () => handleCloseTab(idx)
 | 
			
		||||
    }
 | 
			
		||||
  const renameOption = (idx: number, tab: TabProps): Nullable<ContentMenuOption> => {
 | 
			
		||||
    return (
 | 
			
		||||
      onRenameTab &&
 | 
			
		||||
      !tab.renameDisabled && {
 | 
			
		||||
        type: 'text',
 | 
			
		||||
        label: 'Rename',
 | 
			
		||||
        key: 'rename',
 | 
			
		||||
        onSelect: () => setRenamingTab(idx)
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {!headless && (
 | 
			
		||||
        <Stack
 | 
			
		||||
          css={{
 | 
			
		||||
            gap: '$3',
 | 
			
		||||
            flex: 1,
 | 
			
		||||
            flexWrap: 'nowrap',
 | 
			
		||||
            marginBottom: '$2',
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            overflow: 'auto'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {tabs.map((tab, idx) => (
 | 
			
		||||
            <ContextMenu
 | 
			
		||||
              key={tab.header}
 | 
			
		||||
              options={
 | 
			
		||||
                [closeOption(idx), renameOption(idx, tab)].filter(Boolean) as ContentMenuOption[]
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              <Button
 | 
			
		||||
                role="tab"
 | 
			
		||||
                tabIndex={idx}
 | 
			
		||||
                onClick={() => handleActiveChange(idx, tab.header)}
 | 
			
		||||
                onKeyPress={() => handleActiveChange(idx, tab.header)}
 | 
			
		||||
                outline={active !== idx}
 | 
			
		||||
                size="sm"
 | 
			
		||||
                css={{
 | 
			
		||||
                  '&:hover': {
 | 
			
		||||
                    span: {
 | 
			
		||||
                      visibility: 'visible'
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {tab.header || idx}
 | 
			
		||||
                {onCloseTab && (
 | 
			
		||||
                  <Box
 | 
			
		||||
                    as="span"
 | 
			
		||||
                    css={{
 | 
			
		||||
                      display: 'flex',
 | 
			
		||||
                      p: '2px',
 | 
			
		||||
                      borderRadius: '$full',
 | 
			
		||||
                      mr: '-4px',
 | 
			
		||||
                      '&:hover': {
 | 
			
		||||
                        // boxSizing: "0px 0px 1px",
 | 
			
		||||
                        backgroundColor: '$mauve2',
 | 
			
		||||
                        color: '$mauve12'
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={(ev: React.MouseEvent<HTMLElement>) => {
 | 
			
		||||
                      ev.stopPropagation()
 | 
			
		||||
                      handleCloseTab(idx)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <X size="9px" weight="bold" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                )}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </ContextMenu>
 | 
			
		||||
          ))}
 | 
			
		||||
          {onCreateNewTab && (
 | 
			
		||||
            <Dialog open={isNewtabDialogOpen} onOpenChange={setIsNewtabDialogOpen}>
 | 
			
		||||
              <DialogTrigger asChild>
 | 
			
		||||
                <Button ghost size="sm" css={{ alignItems: 'center', px: '$2', mr: '$3' }}>
 | 
			
		||||
                  <Plus size="16px" /> {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
 | 
			
		||||
                </Button>
 | 
			
		||||
              </DialogTrigger>
 | 
			
		||||
              <DialogContent>
 | 
			
		||||
                <DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                  <Label>{label} name</Label>
 | 
			
		||||
                  <Input
 | 
			
		||||
                    value={tabname}
 | 
			
		||||
                    onChange={e => setTabname(e.target.value)}
 | 
			
		||||
                    onKeyPress={e => {
 | 
			
		||||
                      if (e.key === 'Enter') {
 | 
			
		||||
                        handleCreateTab()
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <ErrorText>{tabnameError}</ErrorText>
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
 | 
			
		||||
                <Flex
 | 
			
		||||
                  css={{
 | 
			
		||||
                    marginTop: 25,
 | 
			
		||||
                    justifyContent: 'flex-end',
 | 
			
		||||
                    gap: '$3'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Button outline>Cancel</Button>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                  <Button variant="primary" onClick={handleCreateTab}>
 | 
			
		||||
                    Create
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                <DialogClose asChild>
 | 
			
		||||
                  <Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
 | 
			
		||||
                    <X size="20px" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </DialogClose>
 | 
			
		||||
              </DialogContent>
 | 
			
		||||
            </Dialog>
 | 
			
		||||
          )}
 | 
			
		||||
          {onRenameTab && (
 | 
			
		||||
            <Dialog open={renamingTab !== null} onOpenChange={() => setRenamingTab(null)}>
 | 
			
		||||
              <DialogContent>
 | 
			
		||||
                <DialogTitle>
 | 
			
		||||
                  Rename <Pre>{tabs[renamingTab || 0]?.header}</Pre>
 | 
			
		||||
                </DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                  <Label>Enter new name</Label>
 | 
			
		||||
                  <Input
 | 
			
		||||
                    value={tabname}
 | 
			
		||||
                    onChange={e => setTabname(e.target.value)}
 | 
			
		||||
                    onKeyPress={e => {
 | 
			
		||||
                      if (e.key === 'Enter') {
 | 
			
		||||
                        handleRenameTab()
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <ErrorText>{tabnameError}</ErrorText>
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
 | 
			
		||||
                <Flex
 | 
			
		||||
                  css={{
 | 
			
		||||
                    marginTop: 25,
 | 
			
		||||
                    justifyContent: 'flex-end',
 | 
			
		||||
                    gap: '$3'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Button outline>Cancel</Button>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                  <Button variant="primary" onClick={handleRenameTab}>
 | 
			
		||||
                    Confirm
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                <DialogClose asChild>
 | 
			
		||||
                  <Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
 | 
			
		||||
                    <X size="20px" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </DialogClose>
 | 
			
		||||
              </DialogContent>
 | 
			
		||||
            </Dialog>
 | 
			
		||||
          )}
 | 
			
		||||
        </Stack>
 | 
			
		||||
      )}
 | 
			
		||||
      {keepAllAlive
 | 
			
		||||
        ? tabs.map((tab, idx) => {
 | 
			
		||||
            // TODO Maybe rule out fragments as children
 | 
			
		||||
            if (!isValidElement(tab.children)) {
 | 
			
		||||
              if (active !== idx) return null
 | 
			
		||||
              return tab.children
 | 
			
		||||
            }
 | 
			
		||||
            let key = tab.children.key || tab.header || idx
 | 
			
		||||
            let { children } = tab
 | 
			
		||||
            let { style, ...props } = children.props
 | 
			
		||||
            return (
 | 
			
		||||
              <children.type
 | 
			
		||||
                key={key}
 | 
			
		||||
                {...props}
 | 
			
		||||
                style={{
 | 
			
		||||
                  ...style,
 | 
			
		||||
                  display: active !== idx ? 'none' : undefined
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          })
 | 
			
		||||
        : tabs[active] && (
 | 
			
		||||
            <Fragment key={tabs[active].header || active}>{tabs[active].children}</Fragment>
 | 
			
		||||
          )}
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,41 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
const Text = styled("span", {
 | 
			
		||||
  fontFamily: "$body",
 | 
			
		||||
  lineHeight: "$body",
 | 
			
		||||
  color: "$text",
 | 
			
		||||
});
 | 
			
		||||
const Text = styled('span', {
 | 
			
		||||
  fontFamily: '$body',
 | 
			
		||||
  lineHeight: '$body',
 | 
			
		||||
  color: '$text',
 | 
			
		||||
  variants: {
 | 
			
		||||
    small: {
 | 
			
		||||
      true: {
 | 
			
		||||
        fontSize: '$xs'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    muted: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: '$mauve9'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: '$error'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    warning: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: '$warning'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    monospace: {
 | 
			
		||||
      true: {
 | 
			
		||||
        fontFamily: '$monospace'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    block: {
 | 
			
		||||
      true: {
 | 
			
		||||
        display: 'block'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Text;
 | 
			
		||||
export default Text
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										113
									
								
								components/Textarea.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,113 @@
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
export const Textarea = styled('textarea', {
 | 
			
		||||
  // Reset
 | 
			
		||||
  appearance: 'none',
 | 
			
		||||
  borderWidth: '0',
 | 
			
		||||
  boxSizing: 'border-box',
 | 
			
		||||
  fontFamily: 'inherit',
 | 
			
		||||
  outline: 'none',
 | 
			
		||||
  width: '100%',
 | 
			
		||||
  flex: '1',
 | 
			
		||||
  backgroundColor: '$mauve4',
 | 
			
		||||
  display: 'inline-flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'center',
 | 
			
		||||
  borderRadius: '$sm',
 | 
			
		||||
  p: '$2',
 | 
			
		||||
  fontSize: '$md',
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: '$mauve12',
 | 
			
		||||
  boxShadow: `0 0 0 1px $colors$mauve8`,
 | 
			
		||||
  WebkitTapHighlightColor: 'rgba(0,0,0,0)',
 | 
			
		||||
  '&::before': {
 | 
			
		||||
    boxSizing: 'border-box'
 | 
			
		||||
  },
 | 
			
		||||
  '&::after': {
 | 
			
		||||
    boxSizing: 'border-box'
 | 
			
		||||
  },
 | 
			
		||||
  fontVariantNumeric: 'tabular-nums',
 | 
			
		||||
 | 
			
		||||
  '&:-webkit-autofill': {
 | 
			
		||||
    boxShadow: 'inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3'
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  '&:-webkit-autofill::first-line': {
 | 
			
		||||
    fontFamily: '$untitled',
 | 
			
		||||
    color: '$mauve12'
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  '&:focus': {
 | 
			
		||||
    boxShadow: `0 0 0 1px $colors$mauve10`,
 | 
			
		||||
    '&:-webkit-autofill': {
 | 
			
		||||
      boxShadow: `0 0 0 1px $colors$mauve10`
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  '&::placeholder': {
 | 
			
		||||
    color: '$mauve9'
 | 
			
		||||
  },
 | 
			
		||||
  '&:disabled': {
 | 
			
		||||
    pointerEvents: 'none',
 | 
			
		||||
    backgroundColor: '$mauve2',
 | 
			
		||||
    color: '$mauve8',
 | 
			
		||||
    cursor: 'not-allowed',
 | 
			
		||||
    '&::placeholder': {
 | 
			
		||||
      color: '$mauve7'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  variants: {
 | 
			
		||||
    variant: {
 | 
			
		||||
      ghost: {
 | 
			
		||||
        boxShadow: 'none',
 | 
			
		||||
        backgroundColor: 'transparent',
 | 
			
		||||
        '@hover': {
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            boxShadow: 'inset 0 0 0 1px $colors$mauve7'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          backgroundColor: '$loContrast',
 | 
			
		||||
          boxShadow: `0 0 0 1px $colors$mauve10`
 | 
			
		||||
        },
 | 
			
		||||
        '&:disabled': {
 | 
			
		||||
          backgroundColor: 'transparent'
 | 
			
		||||
        },
 | 
			
		||||
        '&:read-only': {
 | 
			
		||||
          backgroundColor: 'transparent'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      deep: {
 | 
			
		||||
        backgroundColor: '$deep',
 | 
			
		||||
        boxShadow: 'none'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    state: {
 | 
			
		||||
      invalid: {
 | 
			
		||||
        boxShadow: 'inset 0 0 0 1px $colors$crimson7',
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          boxShadow: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      valid: {
 | 
			
		||||
        boxShadow: 'inset 0 0 0 1px $colors$grass7',
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          boxShadow: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    cursor: {
 | 
			
		||||
      default: {
 | 
			
		||||
        cursor: 'default',
 | 
			
		||||
        '&:focus': {
 | 
			
		||||
          cursor: 'text'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      text: {
 | 
			
		||||
        cursor: 'text'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Textarea
 | 
			
		||||
@@ -1,37 +1,34 @@
 | 
			
		||||
import { useState, useEffect } from "react";
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import { Sun, Moon } from "phosphor-react";
 | 
			
		||||
import { useState, useEffect } from 'react'
 | 
			
		||||
import { useTheme } from 'next-themes'
 | 
			
		||||
import { Sun, Moon } from 'phosphor-react'
 | 
			
		||||
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Button from './Button'
 | 
			
		||||
 | 
			
		||||
const ThemeChanger = () => {
 | 
			
		||||
  const { theme, setTheme } = useTheme();
 | 
			
		||||
  const [mounted, setMounted] = useState(false);
 | 
			
		||||
  useEffect(() => setMounted(true), []);
 | 
			
		||||
  const { theme, setTheme } = useTheme()
 | 
			
		||||
  const [mounted, setMounted] = useState(false)
 | 
			
		||||
  useEffect(() => setMounted(true), [])
 | 
			
		||||
 | 
			
		||||
  if (!mounted) return null;
 | 
			
		||||
  if (!mounted) return null
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
    <Button
 | 
			
		||||
      outline
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        setTheme(theme && theme === "light" ? "dark" : "light");
 | 
			
		||||
        setTheme(theme && theme === 'light' ? 'dark' : 'light')
 | 
			
		||||
      }}
 | 
			
		||||
      css={{
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        marginLeft: "auto",
 | 
			
		||||
        cursor: "pointer",
 | 
			
		||||
        alignItems: "center",
 | 
			
		||||
        justifyContent: "center",
 | 
			
		||||
        color: "$muted",
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        marginLeft: 'auto',
 | 
			
		||||
        cursor: 'pointer',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
        justifyContent: 'center',
 | 
			
		||||
        color: '$muted'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {theme === "dark" ? (
 | 
			
		||||
        <Sun weight="bold" size="16px" />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Moon weight="bold" size="16px" />
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
      {theme === 'dark' ? <Sun size="15px" /> : <Moon size="15px" />}
 | 
			
		||||
    </Button>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ThemeChanger;
 | 
			
		||||
export default ThemeChanger
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										90
									
								
								components/Tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,90 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { styled, keyframes } from '../stitches.config'
 | 
			
		||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
 | 
			
		||||
 | 
			
		||||
const slideUpAndFade = keyframes({
 | 
			
		||||
  '0%': { opacity: 0, transform: 'translateY(2px)' },
 | 
			
		||||
  '100%': { opacity: 1, transform: 'translateY(0)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const slideRightAndFade = keyframes({
 | 
			
		||||
  '0%': { opacity: 0, transform: 'translateX(-2px)' },
 | 
			
		||||
  '100%': { opacity: 1, transform: 'translateX(0)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const slideDownAndFade = keyframes({
 | 
			
		||||
  '0%': { opacity: 0, transform: 'translateY(-2px)' },
 | 
			
		||||
  '100%': { opacity: 1, transform: 'translateY(0)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const slideLeftAndFade = keyframes({
 | 
			
		||||
  '0%': { opacity: 0, transform: 'translateX(2px)' },
 | 
			
		||||
  '100%': { opacity: 1, transform: 'translateX(0)' }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledContent = styled(TooltipPrimitive.Content, {
 | 
			
		||||
  borderRadius: 4,
 | 
			
		||||
  padding: '$2 $3',
 | 
			
		||||
  fontSize: 12,
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: '$text',
 | 
			
		||||
  backgroundColor: '$background',
 | 
			
		||||
  boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
 | 
			
		||||
  '@media (prefers-reduced-motion: no-preference)': {
 | 
			
		||||
    animationDuration: '400ms',
 | 
			
		||||
    animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
 | 
			
		||||
    animationFillMode: 'forwards',
 | 
			
		||||
    willChange: 'transform, opacity',
 | 
			
		||||
    '&[data-state="delayed-open"]': {
 | 
			
		||||
      '&[data-side="top"]': { animationName: slideDownAndFade },
 | 
			
		||||
      '&[data-side="right"]': { animationName: slideLeftAndFade },
 | 
			
		||||
      '&[data-side="bottom"]': { animationName: slideUpAndFade },
 | 
			
		||||
      '&[data-side="left"]': { animationName: slideRightAndFade }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  '.dark &': {
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      '0px 0px 10px 2px rgba(0,0,0,.45), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px'
 | 
			
		||||
  },
 | 
			
		||||
  '.light &': {
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      '0px 0px 10px 2px rgba(0,0,0,.25), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const StyledArrow = styled(TooltipPrimitive.Arrow, {
 | 
			
		||||
  fill: '$background'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
interface ITooltip {
 | 
			
		||||
  content: string
 | 
			
		||||
  open?: boolean
 | 
			
		||||
  defaultOpen?: boolean
 | 
			
		||||
  onOpenChange?: (open: boolean) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Tooltip: React.FC<React.ComponentProps<typeof StyledContent> & ITooltip> = ({
 | 
			
		||||
  children,
 | 
			
		||||
  content,
 | 
			
		||||
  open,
 | 
			
		||||
  defaultOpen = false,
 | 
			
		||||
  onOpenChange,
 | 
			
		||||
  ...rest
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <TooltipPrimitive.Root
 | 
			
		||||
      open={open}
 | 
			
		||||
      defaultOpen={defaultOpen}
 | 
			
		||||
      onOpenChange={onOpenChange}
 | 
			
		||||
      delayDuration={100}
 | 
			
		||||
    >
 | 
			
		||||
      <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
 | 
			
		||||
      <StyledContent side="bottom" align="center" {...rest}>
 | 
			
		||||
        <div dangerouslySetInnerHTML={{ __html: content }} />
 | 
			
		||||
        <StyledArrow offset={5} width={11} height={5} />
 | 
			
		||||
      </StyledContent>
 | 
			
		||||
    </TooltipPrimitive.Root>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Tooltip
 | 
			
		||||
							
								
								
									
										282
									
								
								components/Transaction/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,282 @@
 | 
			
		||||
import { Play } from 'phosphor-react'
 | 
			
		||||
import { FC, useCallback, useEffect } from 'react'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import state from '../../state'
 | 
			
		||||
import {
 | 
			
		||||
  defaultTransactionType,
 | 
			
		||||
  getTxFields,
 | 
			
		||||
  modifyTxState,
 | 
			
		||||
  prepareState,
 | 
			
		||||
  prepareTransaction,
 | 
			
		||||
  SelectOption,
 | 
			
		||||
  TransactionState
 | 
			
		||||
} from '../../state/transactions'
 | 
			
		||||
import { sendTransaction } from '../../state/actions'
 | 
			
		||||
import Box from '../Box'
 | 
			
		||||
import Button from '../Button'
 | 
			
		||||
import Flex from '../Flex'
 | 
			
		||||
import { TxJson } from './json'
 | 
			
		||||
import { TxUI } from './ui'
 | 
			
		||||
import { default as _estimateFee } from '../../utils/estimateFee'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import { combineFlags, extractFlags, transactionFlags } from '../../state/constants/flags'
 | 
			
		||||
import { SetHookData, toHex } from '../../utils/setHook'
 | 
			
		||||
 | 
			
		||||
export interface TransactionProps {
 | 
			
		||||
  header: string
 | 
			
		||||
  state: TransactionState
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props }) => {
 | 
			
		||||
  const { accounts, editorSettings } = useSnapshot(state)
 | 
			
		||||
  const { selectedAccount, selectedTransaction, txIsDisabled, txIsLoading, viewType, editorValue } =
 | 
			
		||||
    txState
 | 
			
		||||
 | 
			
		||||
  const setState = useCallback(
 | 
			
		||||
    (pTx?: Partial<TransactionState>) => {
 | 
			
		||||
      return modifyTxState(header, pTx)
 | 
			
		||||
    },
 | 
			
		||||
    [header]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const prepareOptions = useCallback(
 | 
			
		||||
    (state: Partial<TransactionState> = txState) => {
 | 
			
		||||
      const {
 | 
			
		||||
        selectedTransaction,
 | 
			
		||||
        selectedDestAccount,
 | 
			
		||||
        selectedAccount,
 | 
			
		||||
        txFields,
 | 
			
		||||
        selectedFlags,
 | 
			
		||||
        hookParameters,
 | 
			
		||||
        memos
 | 
			
		||||
      } = state
 | 
			
		||||
 | 
			
		||||
      const TransactionType = selectedTransaction?.value || null
 | 
			
		||||
      const Destination = selectedDestAccount?.value || txFields?.Destination
 | 
			
		||||
      const Account = selectedAccount?.value || null
 | 
			
		||||
      const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
 | 
			
		||||
      const HookParameters = Object.entries(hookParameters || {}).reduce<
 | 
			
		||||
        SetHookData['HookParameters']
 | 
			
		||||
      >((acc, [_, { label, value }]) => {
 | 
			
		||||
        return acc.concat({
 | 
			
		||||
          HookParameter: { HookParameterName: toHex(label), HookParameterValue: toHex(value) }
 | 
			
		||||
        })
 | 
			
		||||
      }, [])
 | 
			
		||||
      const Memos = memos
 | 
			
		||||
        ? Object.entries(memos).reduce<SetHookData['Memos']>((acc, [_, { format, data, type }]) => {
 | 
			
		||||
            return acc?.concat({
 | 
			
		||||
              Memo: { MemoData: toHex(data), MemoFormat: toHex(format), MemoType: toHex(type) }
 | 
			
		||||
            })
 | 
			
		||||
          }, [])
 | 
			
		||||
        : undefined
 | 
			
		||||
 | 
			
		||||
      return prepareTransaction({
 | 
			
		||||
        ...txFields,
 | 
			
		||||
        HookParameters,
 | 
			
		||||
        Flags,
 | 
			
		||||
        TransactionType,
 | 
			
		||||
        Destination,
 | 
			
		||||
        Account,
 | 
			
		||||
        Memos
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    [txState]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const transactionType = selectedTransaction?.value
 | 
			
		||||
    const account = selectedAccount?.value
 | 
			
		||||
    if (!account || !transactionType || txIsLoading) {
 | 
			
		||||
      setState({ txIsDisabled: true })
 | 
			
		||||
    } else {
 | 
			
		||||
      setState({ txIsDisabled: false })
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading])
 | 
			
		||||
 | 
			
		||||
  const getJsonString = useCallback(
 | 
			
		||||
    (state?: Partial<TransactionState>) =>
 | 
			
		||||
      JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
 | 
			
		||||
    [editorSettings.tabSize, prepareOptions]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const saveEditorState = useCallback(
 | 
			
		||||
    (value: string = '', transactionType?: string) => {
 | 
			
		||||
      const pTx = prepareState(value, transactionType)
 | 
			
		||||
      if (pTx) {
 | 
			
		||||
        pTx.editorValue = getJsonString(pTx)
 | 
			
		||||
        return setState(pTx)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [getJsonString, setState]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const submitTest = useCallback(async () => {
 | 
			
		||||
    let st: TransactionState | undefined
 | 
			
		||||
    const tt = txState.selectedTransaction?.value
 | 
			
		||||
    if (viewType === 'json') {
 | 
			
		||||
      st = saveEditorState(editorValue, tt)
 | 
			
		||||
      if (!st) return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const account = accounts.find(acc => acc.address === selectedAccount?.value)
 | 
			
		||||
    if (txIsDisabled) return
 | 
			
		||||
 | 
			
		||||
    setState({ txIsLoading: true })
 | 
			
		||||
    const logPrefix = header ? `${header.split('.')[0]}: ` : undefined
 | 
			
		||||
    try {
 | 
			
		||||
      if (!account) {
 | 
			
		||||
        throw Error('Account must be selected from imported accounts!')
 | 
			
		||||
      }
 | 
			
		||||
      const options = prepareOptions(st)
 | 
			
		||||
 | 
			
		||||
      const fields = getTxFields(options.TransactionType)
 | 
			
		||||
      if (fields.Destination && !options.Destination) {
 | 
			
		||||
        throw Error('Destination account is required!')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await sendTransaction(account, options, { logPrefix })
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(error)
 | 
			
		||||
      if (error instanceof Error) {
 | 
			
		||||
        state.transactionLogs.push({
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          message: `${logPrefix}${error.message}`
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    setState({ txIsLoading: false })
 | 
			
		||||
  }, [
 | 
			
		||||
    txState.selectedTransaction?.value,
 | 
			
		||||
    viewType,
 | 
			
		||||
    accounts,
 | 
			
		||||
    txIsDisabled,
 | 
			
		||||
    setState,
 | 
			
		||||
    header,
 | 
			
		||||
    saveEditorState,
 | 
			
		||||
    editorValue,
 | 
			
		||||
    selectedAccount?.value,
 | 
			
		||||
    prepareOptions
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const resetState = useCallback(
 | 
			
		||||
    (transactionType: SelectOption | undefined = defaultTransactionType) => {
 | 
			
		||||
      const fields = getTxFields(transactionType?.value)
 | 
			
		||||
 | 
			
		||||
      const nwState: Partial<TransactionState> = {
 | 
			
		||||
        viewType,
 | 
			
		||||
        selectedTransaction: transactionType
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (fields.Destination !== undefined) {
 | 
			
		||||
        nwState.selectedDestAccount = null
 | 
			
		||||
        fields.Destination = ''
 | 
			
		||||
      } else {
 | 
			
		||||
        fields.Destination = undefined
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) {
 | 
			
		||||
        nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags)
 | 
			
		||||
        fields.Flags = undefined
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      nwState.txFields = fields
 | 
			
		||||
      const state = modifyTxState(header, nwState, { replaceState: true })
 | 
			
		||||
      const editorValue = getJsonString(state)
 | 
			
		||||
      return setState({ editorValue })
 | 
			
		||||
    },
 | 
			
		||||
    [getJsonString, header, setState, viewType]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const estimateFee = useCallback(
 | 
			
		||||
    async (st?: TransactionState, opts?: { silent?: boolean }) => {
 | 
			
		||||
      const state = st || txState
 | 
			
		||||
      const ptx = prepareOptions(state)
 | 
			
		||||
      const account = accounts.find(acc => acc.address === state.selectedAccount?.value)
 | 
			
		||||
      if (!account) {
 | 
			
		||||
        if (!opts?.silent) {
 | 
			
		||||
          toast.error('Please select account from the list.')
 | 
			
		||||
        }
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ptx.Account = account.address
 | 
			
		||||
      ptx.Sequence = account.sequence
 | 
			
		||||
 | 
			
		||||
      const res = await _estimateFee(ptx, account, opts)
 | 
			
		||||
      const fee = res?.base_fee
 | 
			
		||||
      setState({ estimatedFee: fee })
 | 
			
		||||
      return fee
 | 
			
		||||
    },
 | 
			
		||||
    [accounts, prepareOptions, setState, txState]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const switchToJson = useCallback(() => {
 | 
			
		||||
    const editorValue = getJsonString()
 | 
			
		||||
    setState({ viewType: 'json', editorValue })
 | 
			
		||||
  }, [getJsonString, setState])
 | 
			
		||||
 | 
			
		||||
  const switchToUI = useCallback(() => {
 | 
			
		||||
    setState({ viewType: 'ui' })
 | 
			
		||||
  }, [setState])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}>
 | 
			
		||||
      {viewType === 'json' ? (
 | 
			
		||||
        <TxJson
 | 
			
		||||
          getJsonString={getJsonString}
 | 
			
		||||
          saveEditorState={saveEditorState}
 | 
			
		||||
          header={header}
 | 
			
		||||
          state={txState}
 | 
			
		||||
          setState={setState}
 | 
			
		||||
          estimateFee={estimateFee}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <TxUI
 | 
			
		||||
          switchToJson={switchToJson}
 | 
			
		||||
          state={txState}
 | 
			
		||||
          resetState={resetState}
 | 
			
		||||
          setState={setState}
 | 
			
		||||
          estimateFee={estimateFee}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <Flex
 | 
			
		||||
        row
 | 
			
		||||
        css={{
 | 
			
		||||
          justifyContent: 'space-between',
 | 
			
		||||
          position: 'absolute',
 | 
			
		||||
          left: 0,
 | 
			
		||||
          bottom: 0,
 | 
			
		||||
          width: '100%',
 | 
			
		||||
          mb: '$1'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            if (viewType === 'ui') {
 | 
			
		||||
              switchToJson()
 | 
			
		||||
            } else switchToUI()
 | 
			
		||||
          }}
 | 
			
		||||
          outline
 | 
			
		||||
        >
 | 
			
		||||
          {viewType === 'ui' ? 'EDIT AS JSON' : 'EXIT JSON MODE'}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Flex row>
 | 
			
		||||
          <Button onClick={() => resetState()} outline css={{ mr: '$3' }}>
 | 
			
		||||
            RESET
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="primary"
 | 
			
		||||
            onClick={submitTest}
 | 
			
		||||
            isLoading={txIsLoading}
 | 
			
		||||
            disabled={txIsDisabled}
 | 
			
		||||
          >
 | 
			
		||||
            <Play weight="bold" size="16px" />
 | 
			
		||||
            RUN TEST
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Transaction
 | 
			
		||||
							
								
								
									
										197
									
								
								components/Transaction/json.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,197 @@
 | 
			
		||||
import { FC, useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import state, { transactionsData, TransactionState } from '../../state'
 | 
			
		||||
import Text from '../Text'
 | 
			
		||||
import { Flex, Link } from '..'
 | 
			
		||||
import { showAlert } from '../../state/actions/showAlert'
 | 
			
		||||
import { parseJSON } from '../../utils/json'
 | 
			
		||||
import { extractSchemaProps } from '../../utils/schema'
 | 
			
		||||
import amountSchema from '../../content/amount-schema.json'
 | 
			
		||||
import Monaco from '../Monaco'
 | 
			
		||||
import type monaco from 'monaco-editor'
 | 
			
		||||
 | 
			
		||||
interface JsonProps {
 | 
			
		||||
  getJsonString: (st?: Partial<TransactionState>) => string
 | 
			
		||||
  saveEditorState: (val?: string, tt?: string) => TransactionState | undefined
 | 
			
		||||
  header?: string
 | 
			
		||||
  setState: (pTx?: Partial<TransactionState> | undefined) => void
 | 
			
		||||
  state: TransactionState
 | 
			
		||||
  estimateFee?: () => Promise<string | undefined>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TxJson: FC<JsonProps> = ({
 | 
			
		||||
  getJsonString,
 | 
			
		||||
  state: txState,
 | 
			
		||||
  header,
 | 
			
		||||
  setState,
 | 
			
		||||
  saveEditorState
 | 
			
		||||
}) => {
 | 
			
		||||
  const { editorSettings, accounts } = useSnapshot(state)
 | 
			
		||||
  const { editorValue, estimatedFee, editorIsSaved } = txState
 | 
			
		||||
  const [currTxType, setCurrTxType] = useState<string | undefined>(
 | 
			
		||||
    txState.selectedTransaction?.value
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const parsed = parseJSON(editorValue)
 | 
			
		||||
    if (!parsed) return
 | 
			
		||||
 | 
			
		||||
    const tt = parsed.TransactionType
 | 
			
		||||
    const tx = transactionsData.find(t => t.TransactionType === tt)
 | 
			
		||||
    if (tx) setCurrTxType(tx.TransactionType)
 | 
			
		||||
    else {
 | 
			
		||||
      setCurrTxType(undefined)
 | 
			
		||||
    }
 | 
			
		||||
  }, [editorValue])
 | 
			
		||||
 | 
			
		||||
  const discardChanges = () => {
 | 
			
		||||
    showAlert('Confirm', {
 | 
			
		||||
      body: 'Are you sure to discard these changes?',
 | 
			
		||||
      confirmText: 'Yes',
 | 
			
		||||
      onCancel: () => {},
 | 
			
		||||
      onConfirm: () => setState({ editorValue: getJsonString() })
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onExit = (value: string) => {
 | 
			
		||||
    const options = parseJSON(value)
 | 
			
		||||
    if (options) {
 | 
			
		||||
      saveEditorState(value, currTxType)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    showAlert('Error!', {
 | 
			
		||||
      body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
 | 
			
		||||
      confirmText: 'Discard',
 | 
			
		||||
      onConfirm: () => setState({ editorValue: getJsonString?.() }),
 | 
			
		||||
      onCancel: () => setState({ viewType: 'json' })
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getSchemas = useCallback(async (): Promise<any[]> => {
 | 
			
		||||
    const txObj = transactionsData.find(td => td.TransactionType === currTxType)
 | 
			
		||||
 | 
			
		||||
    let genericSchemaProps: any
 | 
			
		||||
    if (txObj) {
 | 
			
		||||
      genericSchemaProps = extractSchemaProps(txObj)
 | 
			
		||||
    } else {
 | 
			
		||||
      genericSchemaProps = transactionsData.reduce(
 | 
			
		||||
        (cumm, td) => ({
 | 
			
		||||
          ...cumm,
 | 
			
		||||
          ...extractSchemaProps(td)
 | 
			
		||||
        }),
 | 
			
		||||
        {}
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    return [
 | 
			
		||||
      {
 | 
			
		||||
        uri: 'file:///main-schema.json', // id of the first schema
 | 
			
		||||
        fileMatch: ['**.json'], // associate with our model
 | 
			
		||||
        schema: {
 | 
			
		||||
          title: header,
 | 
			
		||||
          type: 'object',
 | 
			
		||||
          required: ['TransactionType', 'Account'],
 | 
			
		||||
          properties: {
 | 
			
		||||
            ...genericSchemaProps,
 | 
			
		||||
            TransactionType: {
 | 
			
		||||
              title: 'Transaction Type',
 | 
			
		||||
              enum: transactionsData.map(td => td.TransactionType)
 | 
			
		||||
            },
 | 
			
		||||
            Account: {
 | 
			
		||||
              $ref: 'file:///account-schema.json'
 | 
			
		||||
            },
 | 
			
		||||
            Destination: {
 | 
			
		||||
              anyOf: [
 | 
			
		||||
                {
 | 
			
		||||
                  $ref: 'file:///account-schema.json'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  type: 'string',
 | 
			
		||||
                  title: 'Destination Account'
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
            },
 | 
			
		||||
            Amount: {
 | 
			
		||||
              $ref: 'file:///amount-schema.json'
 | 
			
		||||
            },
 | 
			
		||||
            Fee: {
 | 
			
		||||
              $ref: 'file:///fee-schema.json'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        uri: 'file:///account-schema.json',
 | 
			
		||||
        schema: {
 | 
			
		||||
          type: 'string',
 | 
			
		||||
          title: 'Account type',
 | 
			
		||||
          enum: accounts.map(acc => acc.address)
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        uri: 'file:///fee-schema.json',
 | 
			
		||||
        schema: {
 | 
			
		||||
          type: 'string',
 | 
			
		||||
          title: 'Fee type',
 | 
			
		||||
          const: estimatedFee,
 | 
			
		||||
          description: estimatedFee ? 'Above mentioned value is recommended base fee' : undefined
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        ...amountSchema
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }, [accounts, currTxType, estimatedFee, header])
 | 
			
		||||
 | 
			
		||||
  const [monacoInst, setMonacoInst] = useState<typeof monaco>()
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!monacoInst) return
 | 
			
		||||
    getSchemas().then(schemas => {
 | 
			
		||||
      monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
 | 
			
		||||
        validate: true,
 | 
			
		||||
        schemas
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  }, [getSchemas, monacoInst])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Monaco
 | 
			
		||||
      rootProps={{
 | 
			
		||||
        css: { height: 'calc(100% - 45px)' }
 | 
			
		||||
      }}
 | 
			
		||||
      language={'json'}
 | 
			
		||||
      id={header}
 | 
			
		||||
      height="100%"
 | 
			
		||||
      value={editorValue}
 | 
			
		||||
      onChange={val => setState({ editorValue: val, editorIsSaved: false })}
 | 
			
		||||
      onMount={(editor, monaco) => {
 | 
			
		||||
        editor.updateOptions({
 | 
			
		||||
          minimap: { enabled: false },
 | 
			
		||||
          glyphMargin: true,
 | 
			
		||||
          tabSize: editorSettings.tabSize,
 | 
			
		||||
          dragAndDrop: true,
 | 
			
		||||
          fontSize: 14
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        setMonacoInst(monaco)
 | 
			
		||||
        // register onExit cb
 | 
			
		||||
        const model = editor.getModel()
 | 
			
		||||
        model?.onWillDispose(() => onExit(model.getValue()))
 | 
			
		||||
      }}
 | 
			
		||||
      overlay={
 | 
			
		||||
        !editorIsSaved ? (
 | 
			
		||||
          <Flex row align="center" css={{ fontSize: '$xs', color: '$textMuted', ml: 'auto' }}>
 | 
			
		||||
            <Text muted small>
 | 
			
		||||
              This file has unsaved changes.
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Link css={{ ml: '$1' }} onClick={() => saveEditorState(editorValue, currTxType)}>
 | 
			
		||||
              save
 | 
			
		||||
            </Link>
 | 
			
		||||
            <Link css={{ ml: '$1' }} onClick={discardChanges}>
 | 
			
		||||
              discard
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        ) : undefined
 | 
			
		||||
      }
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										452
									
								
								components/Transaction/ui.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,452 @@
 | 
			
		||||
import { FC, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
 | 
			
		||||
import Container from '../Container'
 | 
			
		||||
import Flex from '../Flex'
 | 
			
		||||
import Input from '../Input'
 | 
			
		||||
import Select from '../Select'
 | 
			
		||||
import Text from '../Text'
 | 
			
		||||
import {
 | 
			
		||||
  SelectOption,
 | 
			
		||||
  TransactionState,
 | 
			
		||||
  transactionsOptions,
 | 
			
		||||
  TxFields,
 | 
			
		||||
  getTxFields,
 | 
			
		||||
  defaultTransactionType
 | 
			
		||||
} from '../../state/transactions'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import state from '../../state'
 | 
			
		||||
import { streamState } from '../DebugStream'
 | 
			
		||||
import { Button } from '..'
 | 
			
		||||
import Textarea from '../Textarea'
 | 
			
		||||
import { getFlags } from '../../state/constants/flags'
 | 
			
		||||
import { Plus, Trash } from 'phosphor-react'
 | 
			
		||||
import AccountSequence from '../Sequence'
 | 
			
		||||
 | 
			
		||||
interface UIProps {
 | 
			
		||||
  setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
 | 
			
		||||
  resetState: (tt?: SelectOption) => TransactionState | undefined
 | 
			
		||||
  state: TransactionState
 | 
			
		||||
  estimateFee?: (...arg: any) => Promise<string | undefined>
 | 
			
		||||
  switchToJson: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TxUI: FC<UIProps> = ({
 | 
			
		||||
  state: txState,
 | 
			
		||||
  setState,
 | 
			
		||||
  resetState,
 | 
			
		||||
  estimateFee,
 | 
			
		||||
  switchToJson
 | 
			
		||||
}) => {
 | 
			
		||||
  const { accounts } = useSnapshot(state)
 | 
			
		||||
  const {
 | 
			
		||||
    selectedAccount,
 | 
			
		||||
    selectedDestAccount,
 | 
			
		||||
    selectedTransaction,
 | 
			
		||||
    txFields,
 | 
			
		||||
    selectedFlags,
 | 
			
		||||
    hookParameters,
 | 
			
		||||
    memos
 | 
			
		||||
  } = txState
 | 
			
		||||
 | 
			
		||||
  const accountOptions: SelectOption[] = accounts.map(acc => ({
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
    value: acc.address
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  const destAccountOptions: SelectOption[] = accounts
 | 
			
		||||
    .map(acc => ({
 | 
			
		||||
      label: acc.name,
 | 
			
		||||
      value: acc.address
 | 
			
		||||
    }))
 | 
			
		||||
    .filter(acc => acc.value !== selectedAccount?.value)
 | 
			
		||||
 | 
			
		||||
  const flagsOptions: SelectOption[] = Object.entries(
 | 
			
		||||
    getFlags(selectedTransaction?.value) || {}
 | 
			
		||||
  ).map(([label, value]) => ({
 | 
			
		||||
    label,
 | 
			
		||||
    value
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  const [feeLoading, setFeeLoading] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const handleSetAccount = (acc: SelectOption) => {
 | 
			
		||||
    setState({ selectedAccount: acc })
 | 
			
		||||
    streamState.selectedAccount = acc
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSetField = useCallback(
 | 
			
		||||
    (field: keyof TxFields, value: string, opFields?: TxFields) => {
 | 
			
		||||
      const fields = opFields || txFields
 | 
			
		||||
      const obj = fields[field]
 | 
			
		||||
      setState({
 | 
			
		||||
        txFields: {
 | 
			
		||||
          ...fields,
 | 
			
		||||
          [field]: typeof obj === 'object' ? { ...obj, $value: value } : value
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    [setState, txFields]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const handleEstimateFee = useCallback(
 | 
			
		||||
    async (state?: TransactionState, silent?: boolean) => {
 | 
			
		||||
      setFeeLoading(true)
 | 
			
		||||
 | 
			
		||||
      const fee = await estimateFee?.(state, { silent })
 | 
			
		||||
      if (fee) handleSetField('Fee', fee, state?.txFields)
 | 
			
		||||
 | 
			
		||||
      setFeeLoading(false)
 | 
			
		||||
    },
 | 
			
		||||
    [estimateFee, handleSetField]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const handleChangeTxType = useCallback(
 | 
			
		||||
    (tt: SelectOption) => {
 | 
			
		||||
      setState({ selectedTransaction: tt })
 | 
			
		||||
 | 
			
		||||
      const newState = resetState(tt)
 | 
			
		||||
 | 
			
		||||
      handleEstimateFee(newState, true)
 | 
			
		||||
    },
 | 
			
		||||
    [handleEstimateFee, resetState, setState]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  // default tx
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectedTransaction?.value) return
 | 
			
		||||
 | 
			
		||||
    if (defaultTransactionType) {
 | 
			
		||||
      handleChangeTxType(defaultTransactionType)
 | 
			
		||||
    }
 | 
			
		||||
  }, [handleChangeTxType, selectedTransaction?.value])
 | 
			
		||||
 | 
			
		||||
  const fields = useMemo(
 | 
			
		||||
    () => getTxFields(selectedTransaction?.value),
 | 
			
		||||
    [selectedTransaction?.value]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos']
 | 
			
		||||
  if (fields.Destination !== undefined) {
 | 
			
		||||
    richFields.push('Destination')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (flagsOptions.length) {
 | 
			
		||||
    richFields.push('Flags')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
 | 
			
		||||
  return (
 | 
			
		||||
    <Container
 | 
			
		||||
      css={{
 | 
			
		||||
        p: '$3 01',
 | 
			
		||||
        fontSize: '$sm',
 | 
			
		||||
        height: 'calc(100% - 45px)'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}>
 | 
			
		||||
        <TxField label="Transaction type">
 | 
			
		||||
          <Select
 | 
			
		||||
            instanceId="transactionsType"
 | 
			
		||||
            placeholder="Select transaction type"
 | 
			
		||||
            options={transactionsOptions}
 | 
			
		||||
            hideSelectedOptions
 | 
			
		||||
            value={selectedTransaction}
 | 
			
		||||
            onChange={(tt: any) => handleChangeTxType(tt)}
 | 
			
		||||
          />
 | 
			
		||||
        </TxField>
 | 
			
		||||
        <TxField label="Account">
 | 
			
		||||
          <Select
 | 
			
		||||
            instanceId="from-account"
 | 
			
		||||
            placeholder="Select your account"
 | 
			
		||||
            options={accountOptions}
 | 
			
		||||
            value={selectedAccount}
 | 
			
		||||
            onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
 | 
			
		||||
          />
 | 
			
		||||
        </TxField>
 | 
			
		||||
        <TxField label="Sequence">
 | 
			
		||||
          <AccountSequence address={selectedAccount?.value} />
 | 
			
		||||
        </TxField>
 | 
			
		||||
        {richFields.includes('Destination') && (
 | 
			
		||||
          <TxField label="Destination account">
 | 
			
		||||
            <Select
 | 
			
		||||
              instanceId="to-account"
 | 
			
		||||
              placeholder="Select the destination account"
 | 
			
		||||
              options={destAccountOptions}
 | 
			
		||||
              value={selectedDestAccount}
 | 
			
		||||
              isClearable
 | 
			
		||||
              onChange={(acc: any) => setState({ selectedDestAccount: acc })}
 | 
			
		||||
            />
 | 
			
		||||
          </TxField>
 | 
			
		||||
        )}
 | 
			
		||||
        {richFields.includes('Flags') && (
 | 
			
		||||
          <TxField label="Flags">
 | 
			
		||||
            <Select
 | 
			
		||||
              isClearable
 | 
			
		||||
              instanceId="flags"
 | 
			
		||||
              placeholder="Select flags to apply"
 | 
			
		||||
              menuPosition="fixed"
 | 
			
		||||
              value={selectedFlags}
 | 
			
		||||
              isMulti
 | 
			
		||||
              options={flagsOptions}
 | 
			
		||||
              onChange={flags => setState({ selectedFlags: flags as any })}
 | 
			
		||||
              closeMenuOnSelect={
 | 
			
		||||
                selectedFlags ? selectedFlags.length >= flagsOptions.length - 1 : false
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
          </TxField>
 | 
			
		||||
        )}
 | 
			
		||||
        {otherFields.map(field => {
 | 
			
		||||
          let _value = txFields[field]
 | 
			
		||||
 | 
			
		||||
          let value: string | undefined
 | 
			
		||||
          if (typeof _value === 'object') {
 | 
			
		||||
            if (_value.$type === 'json' && typeof _value.$value === 'object') {
 | 
			
		||||
              value = JSON.stringify(_value.$value, null, 2)
 | 
			
		||||
            } else {
 | 
			
		||||
              value = _value.$value.toString()
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            value = _value?.toString()
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const isXrp = typeof _value === 'object' && _value.$type === 'xrp'
 | 
			
		||||
          const isJson = typeof _value === 'object' && _value.$type === 'json'
 | 
			
		||||
          const isFee = field === 'Fee'
 | 
			
		||||
          let rows = isJson ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined
 | 
			
		||||
          if (rows && rows > 5) rows = 5
 | 
			
		||||
          return (
 | 
			
		||||
            <TxField key={field} label={field + (isXrp ? ' (XRP)' : '')}>
 | 
			
		||||
              {isJson ? (
 | 
			
		||||
                <Textarea
 | 
			
		||||
                  rows={rows}
 | 
			
		||||
                  value={value}
 | 
			
		||||
                  spellCheck={false}
 | 
			
		||||
                  onChange={switchToJson}
 | 
			
		||||
                  css={{
 | 
			
		||||
                    flex: 'inherit',
 | 
			
		||||
                    resize: 'vertical'
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <Input
 | 
			
		||||
                  type={isFee ? 'number' : 'text'}
 | 
			
		||||
                  value={value}
 | 
			
		||||
                  onChange={e => {
 | 
			
		||||
                    if (isFee) {
 | 
			
		||||
                      const val = e.target.value.replaceAll('.', '').replaceAll(',', '')
 | 
			
		||||
                      handleSetField(field, val)
 | 
			
		||||
                    } else {
 | 
			
		||||
                      handleSetField(field, e.target.value)
 | 
			
		||||
                    }
 | 
			
		||||
                  }}
 | 
			
		||||
                  onKeyPress={
 | 
			
		||||
                    isFee
 | 
			
		||||
                      ? e => {
 | 
			
		||||
                          if (e.key === '.' || e.key === ',') {
 | 
			
		||||
                            e.preventDefault()
 | 
			
		||||
                          }
 | 
			
		||||
                        }
 | 
			
		||||
                      : undefined
 | 
			
		||||
                  }
 | 
			
		||||
                  css={{
 | 
			
		||||
                    flex: 'inherit',
 | 
			
		||||
                    '-moz-appearance': 'textfield',
 | 
			
		||||
                    '&::-webkit-outer-spin-button': {
 | 
			
		||||
                      '-webkit-appearance': 'none',
 | 
			
		||||
                      margin: 0
 | 
			
		||||
                    },
 | 
			
		||||
                    '&::-webkit-inner-spin-button ': {
 | 
			
		||||
                      '-webkit-appearance': 'none',
 | 
			
		||||
                      margin: 0
 | 
			
		||||
                    }
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
              {isFee && (
 | 
			
		||||
                <Button
 | 
			
		||||
                  size="xs"
 | 
			
		||||
                  variant="primary"
 | 
			
		||||
                  outline
 | 
			
		||||
                  disabled={txState.txIsDisabled}
 | 
			
		||||
                  isDisabled={txState.txIsDisabled}
 | 
			
		||||
                  isLoading={feeLoading}
 | 
			
		||||
                  css={{
 | 
			
		||||
                    position: 'absolute',
 | 
			
		||||
                    right: '$2',
 | 
			
		||||
                    fontSize: '$xs',
 | 
			
		||||
                    cursor: 'pointer',
 | 
			
		||||
                    alignContent: 'center',
 | 
			
		||||
                    display: 'flex'
 | 
			
		||||
                  }}
 | 
			
		||||
                  onClick={() => handleEstimateFee()}
 | 
			
		||||
                >
 | 
			
		||||
                  Suggest
 | 
			
		||||
                </Button>
 | 
			
		||||
              )}
 | 
			
		||||
            </TxField>
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
        <TxField multiLine label="Hook parameters">
 | 
			
		||||
          <Flex column fluid>
 | 
			
		||||
            {Object.entries(hookParameters).map(([id, { label, value }]) => (
 | 
			
		||||
              <Flex column key={id} css={{ mb: '$2' }}>
 | 
			
		||||
                <Flex row>
 | 
			
		||||
                  <Input
 | 
			
		||||
                    placeholder="Parameter name"
 | 
			
		||||
                    value={label}
 | 
			
		||||
                    onChange={e => {
 | 
			
		||||
                      setState({
 | 
			
		||||
                        hookParameters: {
 | 
			
		||||
                          ...hookParameters,
 | 
			
		||||
                          [id]: { label: e.target.value, value }
 | 
			
		||||
                        }
 | 
			
		||||
                      })
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Input
 | 
			
		||||
                    css={{ mx: '$2' }}
 | 
			
		||||
                    placeholder="Value"
 | 
			
		||||
                    value={value}
 | 
			
		||||
                    onChange={e => {
 | 
			
		||||
                      setState({
 | 
			
		||||
                        hookParameters: {
 | 
			
		||||
                          ...hookParameters,
 | 
			
		||||
                          [id]: { label, value: e.target.value }
 | 
			
		||||
                        }
 | 
			
		||||
                      })
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Button
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      const { [id]: _, ...rest } = hookParameters
 | 
			
		||||
                      setState({ hookParameters: rest })
 | 
			
		||||
                    }}
 | 
			
		||||
                    variant="destroy"
 | 
			
		||||
                  >
 | 
			
		||||
                    <Trash weight="regular" size="16px" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Flex>
 | 
			
		||||
              </Flex>
 | 
			
		||||
            ))}
 | 
			
		||||
            <Button
 | 
			
		||||
              outline
 | 
			
		||||
              fullWidth
 | 
			
		||||
              type="button"
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                const id = Object.keys(hookParameters).length
 | 
			
		||||
                setState({
 | 
			
		||||
                  hookParameters: { ...hookParameters, [id]: { label: '', value: '' } }
 | 
			
		||||
                })
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Plus size="16px" />
 | 
			
		||||
              Add Hook Parameter
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </TxField>
 | 
			
		||||
        <TxField multiLine label="Memos">
 | 
			
		||||
          <Flex column fluid>
 | 
			
		||||
            {Object.entries(memos).map(([id, memo]) => (
 | 
			
		||||
              <Flex column key={id} css={{ mb: '$2' }}>
 | 
			
		||||
                <Flex
 | 
			
		||||
                  row
 | 
			
		||||
                  css={{
 | 
			
		||||
                    flexWrap: 'wrap',
 | 
			
		||||
                    width: '100%'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Input
 | 
			
		||||
                    placeholder="Memo type"
 | 
			
		||||
                    value={memo.type}
 | 
			
		||||
                    onChange={e => {
 | 
			
		||||
                      setState({
 | 
			
		||||
                        memos: {
 | 
			
		||||
                          ...memos,
 | 
			
		||||
                          [id]: { ...memo, type: e.target.value }
 | 
			
		||||
                        }
 | 
			
		||||
                      })
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Input
 | 
			
		||||
                    placeholder="Data"
 | 
			
		||||
                    css={{ mx: '$2' }}
 | 
			
		||||
                    value={memo.data}
 | 
			
		||||
                    onChange={e => {
 | 
			
		||||
                      setState({
 | 
			
		||||
                        memos: {
 | 
			
		||||
                          ...memos,
 | 
			
		||||
                          [id]: { ...memo, data: e.target.value }
 | 
			
		||||
                        }
 | 
			
		||||
                      })
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Input
 | 
			
		||||
                    placeholder="Format"
 | 
			
		||||
                    value={memo.format}
 | 
			
		||||
                    onChange={e => {
 | 
			
		||||
                      setState({
 | 
			
		||||
                        memos: {
 | 
			
		||||
                          ...memos,
 | 
			
		||||
                          [id]: { ...memo, format: e.target.value }
 | 
			
		||||
                        }
 | 
			
		||||
                      })
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Button
 | 
			
		||||
                    css={{ ml: '$2' }}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      const { [id]: _, ...rest } = memos
 | 
			
		||||
                      setState({ memos: rest })
 | 
			
		||||
                    }}
 | 
			
		||||
                    variant="destroy"
 | 
			
		||||
                  >
 | 
			
		||||
                    <Trash weight="regular" size="16px" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Flex>
 | 
			
		||||
              </Flex>
 | 
			
		||||
            ))}
 | 
			
		||||
            <Button
 | 
			
		||||
              outline
 | 
			
		||||
              fullWidth
 | 
			
		||||
              type="button"
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                const id = Object.keys(memos).length
 | 
			
		||||
                setState({
 | 
			
		||||
                  memos: { ...memos, [id]: { data: '', format: '', type: '' } }
 | 
			
		||||
                })
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Plus size="16px" />
 | 
			
		||||
              Add Memo
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </TxField>
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </Container>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TxField: FC<{ label: string; children: ReactNode; multiLine?: boolean }> = ({
 | 
			
		||||
  label,
 | 
			
		||||
  children,
 | 
			
		||||
  multiLine = false
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Flex
 | 
			
		||||
      row
 | 
			
		||||
      fluid
 | 
			
		||||
      css={{
 | 
			
		||||
        justifyContent: 'flex-end',
 | 
			
		||||
        alignItems: multiLine ? 'flex-start' : 'center',
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        mb: '$3',
 | 
			
		||||
        mt: '1px',
 | 
			
		||||
        pr: '1px'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Text muted css={{ mr: '$3', mt: multiLine ? '$2' : 0 }}>
 | 
			
		||||
        {label}:{' '}
 | 
			
		||||
      </Text>
 | 
			
		||||
      <Flex css={{ width: '70%', alignItems: 'center' }}>{children}</Flex>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								components/icons/Carbon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
			
		||||
const Carbon = () => (
 | 
			
		||||
  <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 2L23 15H28L21 24H45L38 15H43L33 2Z"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 24V30"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default Carbon
 | 
			
		||||
							
								
								
									
										69
									
								
								components/icons/Firewall.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,69 @@
 | 
			
		||||
const Firewall = () => (
 | 
			
		||||
  <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 13V7"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M27 19V13"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M39 19V13"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 25V19"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M21 13H45"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M21 19H45"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M45 7H21V25H45V7Z"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default Firewall
 | 
			
		||||
							
								
								
									
										34
									
								
								components/icons/Notary.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
			
		||||
const Notary = () => (
 | 
			
		||||
  <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path
 | 
			
		||||
      d="M37.5 10.5L26.5 21.5L21 16.0002"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M49 10.5L38 21.5L35.0784 18.5785"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 5L8.94099 16.0625L4.00543e-05 27.125H2.27587L10.5015 16.9475H16.5938V15.1775H10.5015L2.27582 5H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 5L57.059 16.0625L66 27.125H63.7241L55.4985 16.9475H49.4062V15.1775H55.4985L63.7242 5H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default Notary
 | 
			
		||||
							
								
								
									
										55
									
								
								components/icons/Peggy.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
			
		||||
const Peggy = () => (
 | 
			
		||||
  <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 19C40.1797 19 46 16.3137 46 13C46 9.68629 40.1797 7 33 7C25.8203 7 20 9.68629 20 13C20 16.3137 25.8203 19 33 19Z"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M33 19V25"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M20 13V19C20 22 25 25 33 25C41 25 46 22 46 19V13"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M41 17.7633V23.7634"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M25 17.7633V23.7634"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 4L8.94099 15.0625L4.00543e-05 26.125H2.27587L10.5015 15.9475H16.5938V14.1775H10.5015L2.27582 4H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 4L57.059 15.0625L66 26.125H63.7241L55.4985 15.9475H49.4062V14.1775H55.4985L63.7242 4H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default Peggy
 | 
			
		||||
							
								
								
									
										34
									
								
								components/icons/Starter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
			
		||||
const Starter = () => (
 | 
			
		||||
  <svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path
 | 
			
		||||
      d="M42 28H24C23.7347 28 23.4804 27.8946 23.2929 27.7071C23.1053 27.5196 23 27.2652 23 27V5C23 4.73479 23.1053 4.48044 23.2929 4.2929C23.4804 4.10537 23.7347 4.00001 24 4H36.0003L43 11V27C43 27.2652 42.8947 27.5196 42.7071 27.7071C42.5196 27.8946 42.2653 28 42 28V28Z"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M36 4V11H43.001"
 | 
			
		||||
      stroke="#EDEDEF"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      strokeLinecap="round"
 | 
			
		||||
      strokeLinejoin="round"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M-1.14441e-05 4.875L8.94099 15.9375L4.00543e-05 27H2.27587L10.5015 16.8225H16.5938V15.0525H10.5015L2.27582 4.875H-1.14441e-05Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      className="angle"
 | 
			
		||||
      fillRule="evenodd"
 | 
			
		||||
      clipRule="evenodd"
 | 
			
		||||
      d="M66 4.875L57.059 15.9375L66 27H63.7241L55.4985 16.8225H49.4062V15.0525H55.4985L63.7242 4.875H66Z"
 | 
			
		||||
      fill="#EDEDEF"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default Starter
 | 
			
		||||
							
								
								
									
										16
									
								
								components/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
export { default as Flex } from './Flex'
 | 
			
		||||
export { default as Link } from './Link'
 | 
			
		||||
export { default as Container } from './Container'
 | 
			
		||||
export { default as Heading } from './Heading'
 | 
			
		||||
export { default as Stack } from './Stack'
 | 
			
		||||
export { default as Text } from './Text'
 | 
			
		||||
export { default as Input, Label } from './Input'
 | 
			
		||||
export { default as Select } from './Select'
 | 
			
		||||
export * from './Tabs'
 | 
			
		||||
export * from './AlertDialog/primitive'
 | 
			
		||||
export { default as Box } from './Box'
 | 
			
		||||
export { default as Button } from './Button'
 | 
			
		||||
export { default as Pre } from './Pre'
 | 
			
		||||
export { default as ButtonGroup } from './ButtonGroup'
 | 
			
		||||
export * from './Dialog'
 | 
			
		||||
export * from './DropdownMenu'
 | 
			
		||||
							
								
								
									
										44
									
								
								content/amount-schema.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,44 @@
 | 
			
		||||
{
 | 
			
		||||
  "uri": "file:///amount-schema.json",
 | 
			
		||||
  "title": "Amount",
 | 
			
		||||
  "description": "Specify xrp in drops and tokens as objects.",
 | 
			
		||||
  "schema": {
 | 
			
		||||
    "anyOf": [
 | 
			
		||||
      {
 | 
			
		||||
        "type": ["number", "string"],
 | 
			
		||||
        "exclusiveMinimum": 0,
 | 
			
		||||
        "maximum": "100000000000000000"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "type": "object",
 | 
			
		||||
        "properties": {
 | 
			
		||||
          "currency": {
 | 
			
		||||
            "description": "Arbitrary currency code for the token. Cannot be XRP."
 | 
			
		||||
          },
 | 
			
		||||
          "value": {
 | 
			
		||||
            "type": ["string", "number"],
 | 
			
		||||
            "description": "Quoted decimal representation of the amount of the token."
 | 
			
		||||
          },
 | 
			
		||||
          "issuer": {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "Generally, the account that issues this token. In special cases, this can refer to the account that holds the token instead."
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "defaultSnippets": [
 | 
			
		||||
      {
 | 
			
		||||
        "label": "Xrp",
 | 
			
		||||
        "body": "1000000"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "label": "Token",
 | 
			
		||||
        "body": {
 | 
			
		||||
          "currency": "${1:USD}",
 | 
			
		||||
          "value": "${2:100}",
 | 
			
		||||
          "issuer": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										409
									
								
								content/hook-set-codes.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,409 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "code": 1,
 | 
			
		||||
    "identifier": "AMENDMENT_DISABLED",
 | 
			
		||||
    "description": "attempt to HookSet when amendment is not yet enabled."
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 2,
 | 
			
		||||
    "identifier": "API_ILLEGAL",
 | 
			
		||||
    "description": "HookSet object contained HookApiVersion for existing HookDefinition"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 3,
 | 
			
		||||
    "identifier": "API_INVALID",
 | 
			
		||||
    "description": "HookSet object contained HookApiVersion for unrecognised hook API "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 4,
 | 
			
		||||
    "identifier": "API_MISSING",
 | 
			
		||||
    "description": "HookSet object lacked HookApiVersion"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 5,
 | 
			
		||||
    "identifier": "BLOCK_ILLEGAL",
 | 
			
		||||
    "description": " a block end instruction moves execution below depth 0 {{}}`}` <= like this"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 6,
 | 
			
		||||
    "identifier": "CALL_ILLEGAL",
 | 
			
		||||
    "description": "wasm tries to call a non-whitelisted function"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 7,
 | 
			
		||||
    "identifier": "CALL_INDIRECT",
 | 
			
		||||
    "description": "wasm used call indirect instruction which is disallowed"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 8,
 | 
			
		||||
    "identifier": "CREATE_FLAG",
 | 
			
		||||
    "description": "create operation requires hsoOVERRIDE"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 9,
 | 
			
		||||
    "identifier": "DELETE_FIELD",
 | 
			
		||||
    "description": ""
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 10,
 | 
			
		||||
    "identifier": "DELETE_FLAG",
 | 
			
		||||
    "description": "delete operation requires hsoOVERRIDE"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 11,
 | 
			
		||||
    "identifier": "DELETE_NOTHING",
 | 
			
		||||
    "description": "delete operation would delete nothing"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 12,
 | 
			
		||||
    "identifier": "EXPORTS_MISSING",
 | 
			
		||||
    "description": "hook did not export *any* functions (should be cbak, hook)"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 13,
 | 
			
		||||
    "identifier": "EXPORT_CBAK_FUNC",
 | 
			
		||||
    "description": "hook did not export correct func def int64_t cbak(uint32_t)"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 14,
 | 
			
		||||
    "identifier": "EXPORT_HOOK_FUNC",
 | 
			
		||||
    "description": "hook did not export correct func def int64_t hook(uint32_t)"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "code": 15,
 | 
			
		||||
    "identifier": "EXPORT_MISSING",
 | 
			
		||||
    "description": "distinct from export*S*_missing, either hook or cbak is missing"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "FLAGS_INVALID",
 | 
			
		||||
    "code": 16,
 | 
			
		||||
    "description": "HookSet flags were invalid for specified operation                          "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "FUNCS_MISSING",
 | 
			
		||||
    "code": 17,
 | 
			
		||||
    "description": "hook did not include function code for any functions                        "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "FUNC_PARAM_INVALID",
 | 
			
		||||
    "code": 18,
 | 
			
		||||
    "description": "parameter types may only be i32 i64 u32 u64                                 "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "FUNC_RETURN_COUNT",
 | 
			
		||||
    "code": 19,
 | 
			
		||||
    "description": "a function type is defined in the wasm which returns > 1 return value       "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "FUNC_RETURN_INVALID",
 | 
			
		||||
    "code": 20,
 | 
			
		||||
    "description": "a function type does not return i32 i64 u32 or u64                          "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "FUNC_TYPELESS",
 | 
			
		||||
    "code": 21,
 | 
			
		||||
    "description": "hook defined hook/cbak but their type is not defined in wasm                "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "FUNC_TYPE_INVALID",
 | 
			
		||||
    "code": 22,
 | 
			
		||||
    "description": "malformed and illegal wasm in the func type section                         "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "GRANTS_EMPTY",
 | 
			
		||||
    "code": 23,
 | 
			
		||||
    "description": "HookSet object contained an empty grants array (you should remove it)       "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "GRANTS_EXCESS",
 | 
			
		||||
    "code": 24,
 | 
			
		||||
    "description": "HookSet object cotnained a grants array with too many grants                "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "GRANTS_FIELD",
 | 
			
		||||
    "code": 25,
 | 
			
		||||
    "description": "HookSet object contained a grant without Authorize or HookHash              "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "GRANTS_ILLEGAL",
 | 
			
		||||
    "code": 26,
 | 
			
		||||
    "description": "Hookset object contained grants array which contained a non Grant object    "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "GUARD_IMPORT",
 | 
			
		||||
    "code": 27,
 | 
			
		||||
    "description": "guard import is missing                                                     "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "GUARD_MISSING",
 | 
			
		||||
    "code": 28,
 | 
			
		||||
    "description": "guard call missing at top of loop                                           "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "GUARD_PARAMETERS",
 | 
			
		||||
    "code": 29,
 | 
			
		||||
    "description": "guard called but did not use constant expressions for params                "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HASH_OR_CODE",
 | 
			
		||||
    "code": 30,
 | 
			
		||||
    "description": "HookSet object can contain only one of CreateCode and HookHash              "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOKON_MISSING",
 | 
			
		||||
    "code": 31,
 | 
			
		||||
    "description": "HookSet object did not contain HookOn but should have                       "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOKS_ARRAY_BAD",
 | 
			
		||||
    "code": 32,
 | 
			
		||||
    "description": "attempt to HookSet with a Hooks array containing a non-Hook obj             "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOKS_ARRAY_BLANK",
 | 
			
		||||
    "code": 33,
 | 
			
		||||
    "description": "all hook set objs were blank                                                "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOKS_ARRAY_EMPTY",
 | 
			
		||||
    "code": 34,
 | 
			
		||||
    "description": "attempt to HookSet with an empty Hooks array                                "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOKS_ARRAY_MISSING",
 | 
			
		||||
    "code": 35,
 | 
			
		||||
    "description": "attempt to HookSet without a Hooks array                                    "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOKS_ARRAY_TOO_BIG",
 | 
			
		||||
    "code": 36,
 | 
			
		||||
    "description": "attempt to HookSet with a Hooks array beyond the chain size limit           "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOK_ADD",
 | 
			
		||||
    "code": 37,
 | 
			
		||||
    "description": "Informational: adding ltHook to directory                                   "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOK_DEF_MISSING",
 | 
			
		||||
    "code": 38,
 | 
			
		||||
    "description": "attempt to reference a hook definition (by hash) that is not on ledger      "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOK_DELETE",
 | 
			
		||||
    "code": 39,
 | 
			
		||||
    "description": "unable to delete ltHook from owner                                          "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOK_INVALID_FIELD",
 | 
			
		||||
    "code": 40,
 | 
			
		||||
    "description": "HookSetObj contained an illegal/unexpected field                            "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOK_PARAMS_COUNT",
 | 
			
		||||
    "code": 41,
 | 
			
		||||
    "description": "hookset obj would create too many hook parameters                           "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOK_PARAM_SIZE",
 | 
			
		||||
    "code": 42,
 | 
			
		||||
    "description": "hookset obj sets a parameter or value that exceeds max allowable size       "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "IMPORTS_MISSING",
 | 
			
		||||
    "code": 43,
 | 
			
		||||
    "description": "hook must import guard, and accept/rollback                                 "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "IMPORT_ILLEGAL",
 | 
			
		||||
    "code": 44,
 | 
			
		||||
    "description": "attempted import of a non-whitelisted function                              "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "IMPORT_MODULE_BAD",
 | 
			
		||||
    "code": 45,
 | 
			
		||||
    "description": "hook attempted to specify no or a bad import module                         "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "IMPORT_MODULE_ENV",
 | 
			
		||||
    "code": 46,
 | 
			
		||||
    "description": "hook attempted to specify import module not named env                       "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "IMPORT_NAME_BAD",
 | 
			
		||||
    "code": 47,
 | 
			
		||||
    "description": "import name was too short or too long                                       "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "INSTALL_FLAG",
 | 
			
		||||
    "code": 48,
 | 
			
		||||
    "description": "install operation requires hsoOVERRIDE                                      "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "INSTALL_MISSING",
 | 
			
		||||
    "code": 49,
 | 
			
		||||
    "description": "install operation specifies hookhash which doesn't exist on the ledger      "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "INSTRUCTION_COUNT",
 | 
			
		||||
    "code": 50,
 | 
			
		||||
    "description": "worst case execution instruction count as computed by HookSet               "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "INSTRUCTION_EXCESS",
 | 
			
		||||
    "code": 51,
 | 
			
		||||
    "description": "worst case execution instruction count was too large                        "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "MEMORY_GROW",
 | 
			
		||||
    "code": 52,
 | 
			
		||||
    "description": "memory.grow instruction is present but disallowed                           "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NAMESPACE_MISSING",
 | 
			
		||||
    "code": 53,
 | 
			
		||||
    "description": "HookSet object lacked HookNamespace                                         "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE",
 | 
			
		||||
    "code": 54,
 | 
			
		||||
    "description": "Informational: a namespace is being deleted                                 "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_ACCOUNT",
 | 
			
		||||
    "code": 55,
 | 
			
		||||
    "description": "nsdelete tried to delete ns from a non-existing account                     "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_COUNT",
 | 
			
		||||
    "code": 56,
 | 
			
		||||
    "description": "namespace state count less than 0 / overflow                                "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_DIR",
 | 
			
		||||
    "code": 57,
 | 
			
		||||
    "description": "could not delete directory node in ledger                                   "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_DIRECTORY",
 | 
			
		||||
    "code": 58,
 | 
			
		||||
    "description": "nsdelete operation failed to delete ns directory                            "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_DIR_ENTRY",
 | 
			
		||||
    "code": 59,
 | 
			
		||||
    "description": "nsdelete operation failed due to bad entry in ns directory                  "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_ENTRY",
 | 
			
		||||
    "code": 60,
 | 
			
		||||
    "description": "nsdelete operation failed due to missing hook state entry                   "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_FIELD",
 | 
			
		||||
    "code": 61
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_FLAGS",
 | 
			
		||||
    "code": 62
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_NONSTATE",
 | 
			
		||||
    "code": 63,
 | 
			
		||||
    "description": "nsdelete operation failed due to the presence of a non-hookstate obj        "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "NSDELETE_NOTHING",
 | 
			
		||||
    "code": 64,
 | 
			
		||||
    "description": "hsfNSDELETE provided but nothing to delete                                  "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "OPERATION_INVALID",
 | 
			
		||||
    "code": 65,
 | 
			
		||||
    "description": "could not deduce an operation from the provided hookset obj                 "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "OVERRIDE_MISSING",
 | 
			
		||||
    "code": 66,
 | 
			
		||||
    "description": "HookSet object was trying to update or delete a hook but lacked hsfOVERRIDE "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "PARAMETERS_FIELD",
 | 
			
		||||
    "code": 67,
 | 
			
		||||
    "description": "HookParameters contained a HookParameter with an invalid key in it          "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "PARAMETERS_ILLEGAL",
 | 
			
		||||
    "code": 68,
 | 
			
		||||
    "description": "HookParameters contained something other than a HookParameter               "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "PARAMETERS_NAME",
 | 
			
		||||
    "code": 69,
 | 
			
		||||
    "description": "HookParameters contained a HookParameter which lacked ParameterName field   "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "PARAM_HOOK_CBAK",
 | 
			
		||||
    "code": 70,
 | 
			
		||||
    "description": "hook and cbak must take exactly one u32 parameter                           "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "RETURN_HOOK_CBAK",
 | 
			
		||||
    "code": 71,
 | 
			
		||||
    "description": "hook and cbak must retunr i64                                               "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "SHORT_HOOK",
 | 
			
		||||
    "code": 72,
 | 
			
		||||
    "description": "web assembly byte code ended abruptly                                       "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "TYPE_INVALID",
 | 
			
		||||
    "code": 73,
 | 
			
		||||
    "description": "malformed and illegal wasm specifying an illegal local var type             "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "WASM_BAD_MAGIC",
 | 
			
		||||
    "code": 74,
 | 
			
		||||
    "description": "wasm magic number missing or not wasm                                       "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "WASM_INVALID",
 | 
			
		||||
    "code": 75,
 | 
			
		||||
    "description": "set hook operation would set invalid wasm                                   "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "WASM_PARSE_LOOP",
 | 
			
		||||
    "code": 76,
 | 
			
		||||
    "description": "wasm section parsing resulted in an infinite loop                           "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "WASM_SMOKE_TEST",
 | 
			
		||||
    "code": 77,
 | 
			
		||||
    "description": "Informational: first attempt to load wasm into wasm runtime                 "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "WASM_TEST_FAILURE",
 | 
			
		||||
    "code": 78,
 | 
			
		||||
    "description": "the smoke test failed                                                       "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "WASM_TOO_BIG",
 | 
			
		||||
    "code": 79,
 | 
			
		||||
    "description": "set hook would exceed maximum hook size                                     "
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "WASM_TOO_SMALL",
 | 
			
		||||
    "code": 80
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "WASM_VALIDATION",
 | 
			
		||||
    "code": 81,
 | 
			
		||||
    "description": "a generic error while parsing wasm, usually leb128 overflow"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "identifier": "HOOK_CBAK_DIFF_TYPES",
 | 
			
		||||
    "code": 82,
 | 
			
		||||
    "description": "hook and cbak function definitions were different"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										258
									
								
								content/transactions.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,258 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "AccountDelete",
 | 
			
		||||
    "Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
 | 
			
		||||
    "Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
 | 
			
		||||
    "DestinationTag": 13,
 | 
			
		||||
    "Fee": "2000000",
 | 
			
		||||
    "Sequence": 2470665,
 | 
			
		||||
    "Flags": "2147483648"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "AccountSet",
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Sequence": 5,
 | 
			
		||||
    "Domain": "6578616D706C652E636F6D",
 | 
			
		||||
    "SetFlag": 5,
 | 
			
		||||
    "MessageKey": "03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
 | 
			
		||||
    "TransactionType": "CheckCancel",
 | 
			
		||||
    "CheckID": "49647F0D748DC3FE26BDACBC57F251AADEFFF391403EC9BF87C97F67E9977FB0",
 | 
			
		||||
    "Fee": "12"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
 | 
			
		||||
    "TransactionType": "CheckCash",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
 | 
			
		||||
    "Fee": "12"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "CheckCreate",
 | 
			
		||||
    "Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
 | 
			
		||||
    "Destination": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
 | 
			
		||||
    "SendMax": "100000000",
 | 
			
		||||
    "Expiration": 570113521,
 | 
			
		||||
    "InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B",
 | 
			
		||||
    "DestinationTag": 1,
 | 
			
		||||
    "Fee": "12"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "DepositPreauth",
 | 
			
		||||
    "Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
 | 
			
		||||
    "Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "Flags": "2147483648",
 | 
			
		||||
    "Sequence": 2
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "EscrowCancel",
 | 
			
		||||
    "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "OfferSequence": 7,
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "EscrowCreate",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
 | 
			
		||||
    "CancelAfter": 533257958,
 | 
			
		||||
    "FinishAfter": 533171558,
 | 
			
		||||
    "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
 | 
			
		||||
    "DestinationTag": 23480,
 | 
			
		||||
    "SourceTag": 11747,
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "EscrowFinish",
 | 
			
		||||
    "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "OfferSequence": 7,
 | 
			
		||||
    "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
 | 
			
		||||
    "Fulfillment": "A0028000",
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenMint",
 | 
			
		||||
    "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "NFTokenTaxon": 0,
 | 
			
		||||
    "URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenBurn",
 | 
			
		||||
    "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "NFTokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenAcceptOffer",
 | 
			
		||||
    "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "NFTokenSellOffer": "A2FA1A9911FE2AEF83DAB05F437768E26A301EF899BD31EB85E704B3D528FF18",
 | 
			
		||||
    "NFTokenBuyOffer": "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862",
 | 
			
		||||
    "NFTokenBrokerFee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenCancelOffer",
 | 
			
		||||
    "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "NFTokenOffers": {
 | 
			
		||||
      "$type": "json",
 | 
			
		||||
      "$value": [
 | 
			
		||||
        "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenCreateOffer",
 | 
			
		||||
    "Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
 | 
			
		||||
    "NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Flags": "1",
 | 
			
		||||
    "Destination": "",
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "OfferCancel",
 | 
			
		||||
    "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": "0",
 | 
			
		||||
    "LastLedgerSequence": 7108629,
 | 
			
		||||
    "OfferSequence": 6,
 | 
			
		||||
    "Sequence": 7
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "OfferCreate",
 | 
			
		||||
    "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": "0",
 | 
			
		||||
    "LastLedgerSequence": 7108682,
 | 
			
		||||
    "Sequence": 8,
 | 
			
		||||
    "TakerGets": "6000000",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "Payment",
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": "2147483648",
 | 
			
		||||
    "Sequence": 2
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "PaymentChannelCreate",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
 | 
			
		||||
    "SettleDelay": 86400,
 | 
			
		||||
    "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
 | 
			
		||||
    "CancelAfter": 533171558,
 | 
			
		||||
    "DestinationTag": 23480,
 | 
			
		||||
    "SourceTag": 11747,
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "PaymentChannelFund",
 | 
			
		||||
    "Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "$value": "200",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Expiration": 543171558,
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Flags": "0",
 | 
			
		||||
    "TransactionType": "SetRegularKey",
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Flags": "0",
 | 
			
		||||
    "TransactionType": "SignerListSet",
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "SignerQuorum": 3,
 | 
			
		||||
    "SignerEntries": {
 | 
			
		||||
      "$type": "json",
 | 
			
		||||
      "$value": [
 | 
			
		||||
        {
 | 
			
		||||
          "SignerEntry": {
 | 
			
		||||
            "Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
 | 
			
		||||
            "SignerWeight": 2
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "SignerEntry": {
 | 
			
		||||
            "Account": "rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v",
 | 
			
		||||
            "SignerWeight": 1
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "SignerEntry": {
 | 
			
		||||
            "Account": "raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n",
 | 
			
		||||
            "SignerWeight": 1
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "TicketCreate",
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "Sequence": 381,
 | 
			
		||||
    "TicketCount": 10
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "TrustSet",
 | 
			
		||||
    "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": "262144",
 | 
			
		||||
    "LastLedgerSequence": 8007750,
 | 
			
		||||
    "LimitAmount": {
 | 
			
		||||
      "$type": "json",
 | 
			
		||||
      "$value": {
 | 
			
		||||
        "currency": "USD",
 | 
			
		||||
        "issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
 | 
			
		||||
        "value": "100"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Sequence": 12
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "Invoke",
 | 
			
		||||
    "Fee": "12"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "UriToken",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
// Define general type for useWindowSize hook, which includes width and height
 | 
			
		||||
interface Size {
 | 
			
		||||
  width: number | undefined;
 | 
			
		||||
  height: number | undefined;
 | 
			
		||||
  width: number | undefined
 | 
			
		||||
  height: number | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Hook
 | 
			
		||||
@@ -12,25 +12,25 @@ function useWindowSize(): Size {
 | 
			
		||||
  // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
 | 
			
		||||
  const [windowSize, setWindowSize] = useState<Size>({
 | 
			
		||||
    width: undefined,
 | 
			
		||||
    height: undefined,
 | 
			
		||||
  });
 | 
			
		||||
    height: undefined
 | 
			
		||||
  })
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Handler to call on window resize
 | 
			
		||||
    function handleResize() {
 | 
			
		||||
      // Set window width/height to state
 | 
			
		||||
      setWindowSize({
 | 
			
		||||
        width: window.innerWidth,
 | 
			
		||||
        height: window.innerHeight,
 | 
			
		||||
      });
 | 
			
		||||
        height: window.innerHeight
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    // Add event listener
 | 
			
		||||
    window.addEventListener("resize", handleResize);
 | 
			
		||||
    window.addEventListener('resize', handleResize)
 | 
			
		||||
    // Call handler right away so state gets updated with initial window size
 | 
			
		||||
    handleResize();
 | 
			
		||||
    handleResize()
 | 
			
		||||
    // Remove event listener on cleanup
 | 
			
		||||
    return () => window.removeEventListener("resize", handleResize);
 | 
			
		||||
  }, []); // Empty array ensures that effect is only run on mount
 | 
			
		||||
  return windowSize;
 | 
			
		||||
    return () => window.removeEventListener('resize', handleResize)
 | 
			
		||||
  }, []) // Empty array ensures that effect is only run on mount
 | 
			
		||||
  return windowSize
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default useWindowSize;
 | 
			
		||||
export default useWindowSize
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								next-env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,5 +1,4 @@
 | 
			
		||||
/// <reference types="next" />
 | 
			
		||||
/// <reference types="next/types/global" />
 | 
			
		||||
/// <reference types="next/image-types/global" />
 | 
			
		||||
 | 
			
		||||
// NOTE: This file should not be edited
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,19 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  reactStrictMode: true,
 | 
			
		||||
  images: {
 | 
			
		||||
    domains: ['avatars.githubusercontent.com'],
 | 
			
		||||
    domains: ['avatars.githubusercontent.com']
 | 
			
		||||
  },
 | 
			
		||||
  webpack(config, { isServer }) {
 | 
			
		||||
    config.resolve.alias['vscode'] = require.resolve(
 | 
			
		||||
      '@codingame/monaco-languageclient/lib/vscode-compatibility'
 | 
			
		||||
    )
 | 
			
		||||
    if (!isServer) {
 | 
			
		||||
      config.resolve.fallback.fs = false
 | 
			
		||||
    }
 | 
			
		||||
    config.module.rules.push({
 | 
			
		||||
      test: [/\.md$/, /hook-bundle\.js$/],
 | 
			
		||||
      use: 'raw-loader'
 | 
			
		||||
    })
 | 
			
		||||
    return config
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										69
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -6,30 +6,83 @@
 | 
			
		||||
    "dev": "next dev",
 | 
			
		||||
    "build": "next build",
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint"
 | 
			
		||||
    "lint": "next lint",
 | 
			
		||||
    "format": "prettier --write .",
 | 
			
		||||
    "postinstall": "patch-package && yarn run postinstall-postinstall",
 | 
			
		||||
    "postinstall-postinstall": "./node_modules/.bin/browserify -r ripple-binary-codec -r ripple-keypairs -r ripple-address-codec -r ripple-secret-codec -r ./node_modules/xrpl-accountlib/dist/index.js:xrpl-accountlib -o node_modules/xrpl-accountlib/dist/browser.hook-bundle.js"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@monaco-editor/react": "^4.3.1",
 | 
			
		||||
    "@codingame/monaco-jsonrpc": "^0.3.1",
 | 
			
		||||
    "@codingame/monaco-languageclient": "^0.17.0",
 | 
			
		||||
    "@monaco-editor/react": "^4.4.5",
 | 
			
		||||
    "@octokit/core": "^3.5.1",
 | 
			
		||||
    "@radix-ui/colors": "^0.1.7",
 | 
			
		||||
    "@radix-ui/react-alert-dialog": "^0.1.1",
 | 
			
		||||
    "@radix-ui/react-context-menu": "^0.1.6",
 | 
			
		||||
    "@radix-ui/react-dialog": "^0.1.1",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^0.1.1",
 | 
			
		||||
    "@stitches/react": "^1.2.5",
 | 
			
		||||
    "monaco-editor": "^0.29.1",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^0.1.6",
 | 
			
		||||
    "@radix-ui/react-id": "^0.1.1",
 | 
			
		||||
    "@radix-ui/react-label": "^0.1.5",
 | 
			
		||||
    "@radix-ui/react-popover": "^0.1.6",
 | 
			
		||||
    "@radix-ui/react-switch": "^0.1.5",
 | 
			
		||||
    "@radix-ui/react-tooltip": "^0.1.7",
 | 
			
		||||
    "@stitches/react": "^1.2.8",
 | 
			
		||||
    "base64-js": "^1.5.1",
 | 
			
		||||
    "comment-parser": "^1.3.1",
 | 
			
		||||
    "dinero.js": "^1.9.1",
 | 
			
		||||
    "file-saver": "^2.0.5",
 | 
			
		||||
    "filesize": "^8.0.7",
 | 
			
		||||
    "javascript-time-ago": "^2.3.11",
 | 
			
		||||
    "jszip": "^3.7.1",
 | 
			
		||||
    "lodash.uniqby": "^4.7.0",
 | 
			
		||||
    "lodash.xor": "^4.5.0",
 | 
			
		||||
    "monaco-editor": "^0.33.0",
 | 
			
		||||
    "next": "^12.0.4",
 | 
			
		||||
    "next-auth": "^4.0.0-beta.5",
 | 
			
		||||
    "next-themes": "^0.0.15",
 | 
			
		||||
    "next-auth": "^4.10.3",
 | 
			
		||||
    "next-plausible": "^3.2.0",
 | 
			
		||||
    "next-themes": "^0.1.1",
 | 
			
		||||
    "normalize-url": "^7.0.2",
 | 
			
		||||
    "octokit": "^1.7.0",
 | 
			
		||||
    "pako": "^2.0.4",
 | 
			
		||||
    "patch-package": "^6.4.7",
 | 
			
		||||
    "phosphor-react": "^1.3.1",
 | 
			
		||||
    "postinstall-postinstall": "^2.1.0",
 | 
			
		||||
    "prettier": "^2.7.1",
 | 
			
		||||
    "re-resizable": "^6.9.1",
 | 
			
		||||
    "react": "17.0.2",
 | 
			
		||||
    "react-dom": "17.0.2",
 | 
			
		||||
    "react-hook-form": "^7.28.0",
 | 
			
		||||
    "react-hot-keys": "^2.7.1",
 | 
			
		||||
    "react-hot-toast": "^2.1.1",
 | 
			
		||||
    "valtio": "^1.2.5"
 | 
			
		||||
    "react-markdown": "^8.0.3",
 | 
			
		||||
    "react-new-window": "^0.2.1",
 | 
			
		||||
    "react-select": "^5.2.1",
 | 
			
		||||
    "react-split": "^2.0.14",
 | 
			
		||||
    "react-stay-scrolled": "^7.4.0",
 | 
			
		||||
    "react-time-ago": "^7.1.9",
 | 
			
		||||
    "reconnecting-websocket": "^4.4.0",
 | 
			
		||||
    "regexify-string": "^1.0.17",
 | 
			
		||||
    "valtio": "^1.2.5",
 | 
			
		||||
    "vscode-languageserver": "^7.0.0",
 | 
			
		||||
    "vscode-uri": "^3.0.2",
 | 
			
		||||
    "wabt": "^1.0.30",
 | 
			
		||||
    "xrpl-accountlib": "^1.6.1",
 | 
			
		||||
    "xrpl-client": "^2.0.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/dinero.js": "^1.9.0",
 | 
			
		||||
    "@types/file-saver": "^2.0.4",
 | 
			
		||||
    "@types/lodash.uniqby": "^4.7.6",
 | 
			
		||||
    "@types/lodash.xor": "^4.5.6",
 | 
			
		||||
    "@types/pako": "^1.0.2",
 | 
			
		||||
    "@types/react": "17.0.31",
 | 
			
		||||
    "browserify": "^17.0.0",
 | 
			
		||||
    "eslint": "7.32.0",
 | 
			
		||||
    "eslint-config-next": "11.1.2",
 | 
			
		||||
    "raw-loader": "^4.0.2",
 | 
			
		||||
    "typescript": "4.4.4"
 | 
			
		||||
  },
 | 
			
		||||
  "resolutions": {
 | 
			
		||||
    "ripple-binary-codec": "=1.4.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										158
									
								
								pages/_app.tsx
									
									
									
									
									
								
							
							
						
						@@ -1,31 +1,141 @@
 | 
			
		||||
import "../styles/globals.css";
 | 
			
		||||
import type { AppProps } from "next/app";
 | 
			
		||||
import { SessionProvider } from "next-auth/react";
 | 
			
		||||
import { ThemeProvider } from "next-themes";
 | 
			
		||||
import { Toaster } from "react-hot-toast";
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
import '../styles/globals.css'
 | 
			
		||||
import type { AppProps } from 'next/app'
 | 
			
		||||
import Head from 'next/head'
 | 
			
		||||
import { SessionProvider } from 'next-auth/react'
 | 
			
		||||
import { ThemeProvider } from 'next-themes'
 | 
			
		||||
import { Toaster } from 'react-hot-toast'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { IdProvider } from '@radix-ui/react-id'
 | 
			
		||||
import PlausibleProvider from 'next-plausible'
 | 
			
		||||
 | 
			
		||||
import { darkTheme } from "../stitches.config";
 | 
			
		||||
import Navigation from "../components/Navigation";
 | 
			
		||||
import { darkTheme, css } from '../stitches.config'
 | 
			
		||||
import Navigation from '../components/Navigation'
 | 
			
		||||
import { fetchFiles } from '../state/actions'
 | 
			
		||||
import state from '../state'
 | 
			
		||||
 | 
			
		||||
import TimeAgo from 'javascript-time-ago'
 | 
			
		||||
import en from 'javascript-time-ago/locale/en.json'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import Alert from '../components/AlertDialog'
 | 
			
		||||
import { Button, Flex } from '../components'
 | 
			
		||||
import { ChatCircleText } from 'phosphor-react'
 | 
			
		||||
 | 
			
		||||
TimeAgo.setDefaultLocale(en.locale)
 | 
			
		||||
TimeAgo.addLocale(en)
 | 
			
		||||
 | 
			
		||||
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const slug = router.query?.slug
 | 
			
		||||
  const gistId = (Array.isArray(slug) && slug[0]) ?? null
 | 
			
		||||
 | 
			
		||||
  const origin = 'https://xrpl-hooks-ide.vercel.app' // TODO: Change when site is deployed
 | 
			
		||||
  const shareImg = '/share-image.png'
 | 
			
		||||
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (gistId && router.isReady) {
 | 
			
		||||
      fetchFiles(gistId)
 | 
			
		||||
    } else {
 | 
			
		||||
      if (
 | 
			
		||||
        !gistId &&
 | 
			
		||||
        router.isReady &&
 | 
			
		||||
        router.pathname.includes('/develop') &&
 | 
			
		||||
        !snap.files.length &&
 | 
			
		||||
        !snap.mainModalShowed
 | 
			
		||||
      ) {
 | 
			
		||||
        state.mainModalOpen = true
 | 
			
		||||
        state.mainModalShowed = true
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [gistId, router.isReady, router.pathname, snap.files, snap.mainModalShowed])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <SessionProvider session={session}>
 | 
			
		||||
        <ThemeProvider
 | 
			
		||||
          attribute="class"
 | 
			
		||||
          defaultTheme="dark"
 | 
			
		||||
          enableSystem={false}
 | 
			
		||||
          value={{
 | 
			
		||||
            light: "light",
 | 
			
		||||
            dark: darkTheme.className,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Navigation />
 | 
			
		||||
          <Component {...pageProps} />
 | 
			
		||||
          <Toaster />
 | 
			
		||||
        </ThemeProvider>
 | 
			
		||||
      </SessionProvider>
 | 
			
		||||
      <Head>
 | 
			
		||||
        <meta charSet="utf-8" />
 | 
			
		||||
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
 | 
			
		||||
        <meta name="format-detection" content="telephone=no" />
 | 
			
		||||
        <meta property="og:url" content={`${origin}${router.asPath}`} />
 | 
			
		||||
 | 
			
		||||
        <title>XRPL Hooks Builder</title>
 | 
			
		||||
        <meta property="og:title" content="XRPL Hooks Builder" />
 | 
			
		||||
        <meta name="twitter:title" content="XRPL Hooks Builder" />
 | 
			
		||||
        <meta name="twitter:card" content="summary_large_image" />
 | 
			
		||||
        <meta name="twitter:site" content="@XRPLF" />
 | 
			
		||||
        <meta
 | 
			
		||||
          name="description"
 | 
			
		||||
          content="Hooks Builder, add smart contract functionality to the XRP Ledger."
 | 
			
		||||
        />
 | 
			
		||||
        <meta
 | 
			
		||||
          property="og:description"
 | 
			
		||||
          content="Hooks Builder, add smart contract functionality to the XRP Ledger."
 | 
			
		||||
        />
 | 
			
		||||
        <meta
 | 
			
		||||
          name="twitter:description"
 | 
			
		||||
          content="Hooks Builder, add smart contract functionality to the XRP Ledger."
 | 
			
		||||
        />
 | 
			
		||||
        <meta property="og:image" content={`${origin}${shareImg}`} />
 | 
			
		||||
        <meta property="og:image:width" content="1200" />
 | 
			
		||||
        <meta property="og:image:height" content="630" />
 | 
			
		||||
        <meta name="twitter:image" content={`${origin}${shareImg}`} />
 | 
			
		||||
        <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
 | 
			
		||||
        <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
 | 
			
		||||
        <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
 | 
			
		||||
        <link rel="manifest" href="/site.webmanifest" />
 | 
			
		||||
        <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
 | 
			
		||||
        <meta name="application-name" content="XRPL Hooks Builder" />
 | 
			
		||||
        <meta name="msapplication-TileColor" content="#c10ad0" />
 | 
			
		||||
        <meta name="theme-color" content="#161618" media="(prefers-color-scheme: dark)" />
 | 
			
		||||
        <meta name="theme-color" content="#FDFCFD" media="(prefers-color-scheme: light)" />
 | 
			
		||||
      </Head>
 | 
			
		||||
 | 
			
		||||
      <IdProvider>
 | 
			
		||||
        <SessionProvider session={session}>
 | 
			
		||||
          <ThemeProvider
 | 
			
		||||
            attribute="class"
 | 
			
		||||
            defaultTheme="dark"
 | 
			
		||||
            enableSystem={false}
 | 
			
		||||
            value={{
 | 
			
		||||
              light: 'light',
 | 
			
		||||
              dark: darkTheme.className
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <PlausibleProvider domain="hooks-builder.xrpl.org" trackOutboundLinks>
 | 
			
		||||
              <Navigation />
 | 
			
		||||
              <Component {...pageProps} />
 | 
			
		||||
              <Toaster
 | 
			
		||||
                toastOptions={{
 | 
			
		||||
                  className: css({
 | 
			
		||||
                    backgroundColor: '$mauve1',
 | 
			
		||||
                    color: '$mauve10',
 | 
			
		||||
                    fontSize: '$sm',
 | 
			
		||||
                    zIndex: 9999,
 | 
			
		||||
                    '.dark &': {
 | 
			
		||||
                      backgroundColor: '$mauve4',
 | 
			
		||||
                      color: '$mauve12'
 | 
			
		||||
                    }
 | 
			
		||||
                  })()
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              <Alert />
 | 
			
		||||
              <Flex
 | 
			
		||||
                as="a"
 | 
			
		||||
                href="https://github.com/XRPLF/Hooks/discussions"
 | 
			
		||||
                target="_blank"
 | 
			
		||||
                rel="noopener noreferrer"
 | 
			
		||||
                css={{ position: 'fixed', right: '$4', bottom: '$4' }}
 | 
			
		||||
              >
 | 
			
		||||
                <Button size="sm" variant="primary" outline>
 | 
			
		||||
                  <ChatCircleText size={14} style={{ marginRight: '0px' }} />
 | 
			
		||||
                  Bugs & Discussions
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Flex>
 | 
			
		||||
            </PlausibleProvider>
 | 
			
		||||
          </ThemeProvider>
 | 
			
		||||
        </SessionProvider>
 | 
			
		||||
      </IdProvider>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
export default MyApp;
 | 
			
		||||
export default MyApp
 | 
			
		||||
 
 | 
			
		||||
@@ -1,48 +1,34 @@
 | 
			
		||||
import Document, {
 | 
			
		||||
  Html,
 | 
			
		||||
  Head,
 | 
			
		||||
  Main,
 | 
			
		||||
  NextScript,
 | 
			
		||||
  DocumentContext,
 | 
			
		||||
} from "next/document";
 | 
			
		||||
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
 | 
			
		||||
 | 
			
		||||
import { globalStyles, getCssText } from "../stitches.config";
 | 
			
		||||
import { globalStyles, getCssText } from '../stitches.config'
 | 
			
		||||
 | 
			
		||||
class MyDocument extends Document {
 | 
			
		||||
  static async getInitialProps(ctx: DocumentContext) {
 | 
			
		||||
    const initialProps = await Document.getInitialProps(ctx);
 | 
			
		||||
    const initialProps = await Document.getInitialProps(ctx)
 | 
			
		||||
 | 
			
		||||
    return initialProps;
 | 
			
		||||
    return initialProps
 | 
			
		||||
  }
 | 
			
		||||
  render() {
 | 
			
		||||
    globalStyles();
 | 
			
		||||
    globalStyles()
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Html>
 | 
			
		||||
        <Head>
 | 
			
		||||
          <meta name="description" content="Playground for XRPL Hooks" />
 | 
			
		||||
          <link rel="icon" href="/favicon.ico" />
 | 
			
		||||
          <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
 | 
			
		||||
          <link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
          <link
 | 
			
		||||
            rel="preconnect"
 | 
			
		||||
            href="https://fonts.gstatic.com"
 | 
			
		||||
            crossOrigin=""
 | 
			
		||||
          />
 | 
			
		||||
          <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
 | 
			
		||||
          <link
 | 
			
		||||
            href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&family=Work+Sans:wght@400;600;700&display=swap"
 | 
			
		||||
            rel="stylesheet"
 | 
			
		||||
          />
 | 
			
		||||
          <style
 | 
			
		||||
            id="stitches"
 | 
			
		||||
            dangerouslySetInnerHTML={{ __html: getCssText() }}
 | 
			
		||||
          />
 | 
			
		||||
        </Head>
 | 
			
		||||
        <body>
 | 
			
		||||
          <Main />
 | 
			
		||||
          <NextScript />
 | 
			
		||||
        </body>
 | 
			
		||||
      </Html>
 | 
			
		||||
    );
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MyDocument;
 | 
			
		||||
export default MyDocument
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,10 @@
 | 
			
		||||
import type { NextRequest, NextFetchEvent } from 'next/server';
 | 
			
		||||
import { NextResponse as Response } from 'next/server';
 | 
			
		||||
import { getToken } from "next-auth/jwt"
 | 
			
		||||
import type { NextRequest, NextFetchEvent } from 'next/server'
 | 
			
		||||
import { NextResponse as Response } from 'next/server'
 | 
			
		||||
 | 
			
		||||
export default function middleware(req: NextRequest, ev: NextFetchEvent) {
 | 
			
		||||
 | 
			
		||||
  if (req.nextUrl.pathname === "/") {
 | 
			
		||||
    return Response.redirect("/develop");
 | 
			
		||||
 | 
			
		||||
  if (req.nextUrl.pathname === '/') {
 | 
			
		||||
    const url = req.nextUrl.clone()
 | 
			
		||||
    url.pathname = '/develop'
 | 
			
		||||
    return Response.redirect(url)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import NextAuth from "next-auth"
 | 
			
		||||
import GithubProvider from "next-auth/providers/github"
 | 
			
		||||
import NextAuth from 'next-auth'
 | 
			
		||||
 | 
			
		||||
export default NextAuth({
 | 
			
		||||
  // Configure one or more authentication providers
 | 
			
		||||
@@ -11,40 +10,38 @@ export default NextAuth({
 | 
			
		||||
    //   scope: 'user,gist'
 | 
			
		||||
    // }),
 | 
			
		||||
    {
 | 
			
		||||
      id: "github",
 | 
			
		||||
      name: "GitHub",
 | 
			
		||||
      type: "oauth",
 | 
			
		||||
      id: 'github',
 | 
			
		||||
      name: 'GitHub',
 | 
			
		||||
      type: 'oauth',
 | 
			
		||||
      clientId: process.env.GITHUB_ID,
 | 
			
		||||
      clientSecret: process.env.GITHUB_SECRET,
 | 
			
		||||
      authorization: "https://github.com/login/oauth/authorize?scope=read:user+user:email+gist",
 | 
			
		||||
      token: "https://github.com/login/oauth/access_token",
 | 
			
		||||
      userinfo: "https://api.github.com/user",
 | 
			
		||||
      authorization: 'https://github.com/login/oauth/authorize?scope=read:user+user:email+gist',
 | 
			
		||||
      token: 'https://github.com/login/oauth/access_token',
 | 
			
		||||
      userinfo: 'https://api.github.com/user',
 | 
			
		||||
      profile(profile) {
 | 
			
		||||
        return {
 | 
			
		||||
          id: profile.id.toString(),
 | 
			
		||||
          name: profile.name || profile.login,
 | 
			
		||||
          username: profile.login,
 | 
			
		||||
          email: profile.email,
 | 
			
		||||
          image: profile.avatar_url,
 | 
			
		||||
          image: profile.avatar_url
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // ...add more providers here
 | 
			
		||||
  ],
 | 
			
		||||
  callbacks: {
 | 
			
		||||
    async jwt({ token, user, account, profile, isNewUser }) {
 | 
			
		||||
      if (account && account.access_token) {
 | 
			
		||||
        token.accessToken = account.access_token;
 | 
			
		||||
        token.username = user?.username || '';
 | 
			
		||||
        token.accessToken = account.access_token
 | 
			
		||||
        token.username = user?.username || ''
 | 
			
		||||
      }
 | 
			
		||||
      return token
 | 
			
		||||
    },
 | 
			
		||||
    async session({ session, token }) {
 | 
			
		||||
      session.accessToken = token.accessToken as string;
 | 
			
		||||
      const user = { ...session.user, username: token.username };
 | 
			
		||||
      session['user']['username'] = token.username as string;
 | 
			
		||||
      session.accessToken = token.accessToken as string
 | 
			
		||||
      session['user']['username'] = token.username as string
 | 
			
		||||
      return session
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								pages/api/faucet.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
			
		||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
 | 
			
		||||
import type { NextApiRequest, NextApiResponse } from 'next'
 | 
			
		||||
 | 
			
		||||
interface ErrorResponse {
 | 
			
		||||
  error: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Faucet {
 | 
			
		||||
  address: string
 | 
			
		||||
  secret: string
 | 
			
		||||
  xrp: number
 | 
			
		||||
  hash: string
 | 
			
		||||
  code: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function handler(
 | 
			
		||||
  req: NextApiRequest,
 | 
			
		||||
  res: NextApiResponse<Faucet | ErrorResponse>
 | 
			
		||||
) {
 | 
			
		||||
  if (req.method !== 'POST') {
 | 
			
		||||
    return res.status(405).json({ error: 'Method not allowed!' })
 | 
			
		||||
  }
 | 
			
		||||
  const { account } = req.query
 | 
			
		||||
  const ip = Array.isArray(req?.headers?.['x-real-ip'])
 | 
			
		||||
    ? req?.headers?.['x-real-ip'][0]
 | 
			
		||||
    : req?.headers?.['x-real-ip']
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await fetch(
 | 
			
		||||
      `https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`,
 | 
			
		||||
      {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'x-forwarded-for': ip || ''
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
    const json: Faucet | ErrorResponse = await response.json()
 | 
			
		||||
    if ('error' in json) {
 | 
			
		||||
      return res.status(429).json(json)
 | 
			
		||||
    }
 | 
			
		||||
    return res.status(200).json(json)
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.log(err)
 | 
			
		||||
    return res.status(500).json({ error: 'Server error' })
 | 
			
		||||
  }
 | 
			
		||||
  return res.status(500).json({ error: 'Not able to create faucet, try again' })
 | 
			
		||||
}
 | 
			
		||||
@@ -5,9 +5,6 @@ type Data = {
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function handler(
 | 
			
		||||
  req: NextApiRequest,
 | 
			
		||||
  res: NextApiResponse<Data>
 | 
			
		||||
) {
 | 
			
		||||
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
 | 
			
		||||
  res.status(200).json({ name: 'John Doe' })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								pages/api/proxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
import type { NextApiRequest, NextApiResponse } from 'next'
 | 
			
		||||
 | 
			
		||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { url, opts } = req.body
 | 
			
		||||
    const r = await fetch(url, opts)
 | 
			
		||||
    if (!r.ok) throw r.statusText
 | 
			
		||||
 | 
			
		||||
    const data = await r.json()
 | 
			
		||||
    return res.json(data)
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.warn(error)
 | 
			
		||||
    return res.status(500).json({ message: 'Something went wrong!' })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								pages/deploy/[[...slug]].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,59 @@
 | 
			
		||||
import dynamic from 'next/dynamic'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import Split from 'react-split'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import state from '../../state'
 | 
			
		||||
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
 | 
			
		||||
 | 
			
		||||
const DeployEditor = dynamic(() => import('../../components/DeployEditor'), {
 | 
			
		||||
  ssr: false
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const Accounts = dynamic(() => import('../../components/Accounts'), {
 | 
			
		||||
  ssr: false
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const LogBox = dynamic(() => import('../../components/LogBox'), {
 | 
			
		||||
  ssr: false
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const Deploy = () => {
 | 
			
		||||
  const { deployLogs } = useSnapshot(state)
 | 
			
		||||
  return (
 | 
			
		||||
    <Split
 | 
			
		||||
      direction="vertical"
 | 
			
		||||
      gutterSize={4}
 | 
			
		||||
      gutterAlign="center"
 | 
			
		||||
      sizes={getSplit('deployVertical') || [40, 60]}
 | 
			
		||||
      style={{ height: 'calc(100vh - 60px)' }}
 | 
			
		||||
      onDragEnd={e => saveSplit('deployVertical', e)}
 | 
			
		||||
    >
 | 
			
		||||
      <main style={{ display: 'flex', flex: 1, position: 'relative' }}>
 | 
			
		||||
        <DeployEditor />
 | 
			
		||||
      </main>
 | 
			
		||||
      <Split
 | 
			
		||||
        direction="horizontal"
 | 
			
		||||
        sizes={getSplit('deployHorizontal') || [50, 50]}
 | 
			
		||||
        minSize={[320, 160]}
 | 
			
		||||
        gutterSize={4}
 | 
			
		||||
        gutterAlign="center"
 | 
			
		||||
        style={{
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          flexDirection: 'row',
 | 
			
		||||
          width: '100%',
 | 
			
		||||
          height: '100%'
 | 
			
		||||
        }}
 | 
			
		||||
        onDragEnd={e => saveSplit('deployHorizontal', e)}
 | 
			
		||||
      >
 | 
			
		||||
        <div style={{ alignItems: 'stretch', display: 'flex' }}>
 | 
			
		||||
          <Accounts />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div>
 | 
			
		||||
          <LogBox title="Deploy Log" logs={deployLogs} clearLog={() => (state.deployLogs = [])} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </Split>
 | 
			
		||||
    </Split>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Deploy
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
import Container from "../../components/Container";
 | 
			
		||||
 | 
			
		||||
const Deploy = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Container css={{ py: "$10" }}>This will be the deploy page</Container>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Deploy;
 | 
			
		||||
							
								
								
									
										251
									
								
								pages/develop/[[...slug]].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,251 @@
 | 
			
		||||
import { Label } from '@radix-ui/react-label'
 | 
			
		||||
import type { NextPage } from 'next'
 | 
			
		||||
import dynamic from 'next/dynamic'
 | 
			
		||||
import { FileJs, Gear, Play } from 'phosphor-react'
 | 
			
		||||
import Hotkeys from 'react-hot-keys'
 | 
			
		||||
import Split from 'react-split'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import { ButtonGroup, Flex } from '../../components'
 | 
			
		||||
import Box from '../../components/Box'
 | 
			
		||||
import Button from '../../components/Button'
 | 
			
		||||
import Popover from '../../components/Popover'
 | 
			
		||||
import RunScript from '../../components/RunScript'
 | 
			
		||||
import state, { IFile } from '../../state'
 | 
			
		||||
import { compileCode } from '../../state/actions'
 | 
			
		||||
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
 | 
			
		||||
import { styled } from '../../stitches.config'
 | 
			
		||||
import { getFileExtention } from '../../utils/helpers'
 | 
			
		||||
 | 
			
		||||
const HooksEditor = dynamic(() => import('../../components/HooksEditor'), {
 | 
			
		||||
  ssr: false
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const LogBox = dynamic(() => import('../../components/LogBox'), {
 | 
			
		||||
  ssr: false
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const OptimizationText = () => (
 | 
			
		||||
  <span>
 | 
			
		||||
    Specify which optimization level to use for compiling. For example -O0 means “no optimization”:
 | 
			
		||||
    this level compiles the fastest and generates the most debuggable code. -O2 means moderate level
 | 
			
		||||
    of optimization which enables most optimizations. Read more about the options from{' '}
 | 
			
		||||
    <a
 | 
			
		||||
      className="link"
 | 
			
		||||
      rel="noopener noreferrer"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://clang.llvm.org/docs/CommandGuide/clang.html#cmdoption-o0"
 | 
			
		||||
    >
 | 
			
		||||
      clang documentation
 | 
			
		||||
    </a>
 | 
			
		||||
    .
 | 
			
		||||
  </span>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const StyledOptimizationText = styled(OptimizationText, {
 | 
			
		||||
  color: '$mauve12 !important',
 | 
			
		||||
  fontSize: '200px',
 | 
			
		||||
  'span a.link': {
 | 
			
		||||
    color: 'red'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const CompilerSettings = () => {
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  return (
 | 
			
		||||
    <Flex css={{ minWidth: 200, flexDirection: 'column', gap: '$5' }}>
 | 
			
		||||
      <Box>
 | 
			
		||||
        <Label
 | 
			
		||||
          style={{
 | 
			
		||||
            flexDirection: 'row',
 | 
			
		||||
            display: 'flex'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          Optimization level{' '}
 | 
			
		||||
          <Popover
 | 
			
		||||
            css={{
 | 
			
		||||
              maxWidth: '240px',
 | 
			
		||||
              lineHeight: '1.3',
 | 
			
		||||
              a: {
 | 
			
		||||
                color: '$purple11'
 | 
			
		||||
              },
 | 
			
		||||
              '.dark &': {
 | 
			
		||||
                backgroundColor: '$black !important',
 | 
			
		||||
 | 
			
		||||
                '.arrow': {
 | 
			
		||||
                  fill: '$colors$black'
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            content={<StyledOptimizationText />}
 | 
			
		||||
          >
 | 
			
		||||
            <Flex
 | 
			
		||||
              css={{
 | 
			
		||||
                position: 'relative',
 | 
			
		||||
                top: '-1px',
 | 
			
		||||
                ml: '$1',
 | 
			
		||||
                backgroundColor: '$mauve8',
 | 
			
		||||
                borderRadius: '$full',
 | 
			
		||||
                cursor: 'pointer',
 | 
			
		||||
                width: '16px',
 | 
			
		||||
                height: '16px',
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                justifyContent: 'center'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              ?
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Popover>
 | 
			
		||||
        </Label>
 | 
			
		||||
        <ButtonGroup css={{ mt: '$2', fontFamily: '$monospace' }}>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: '$monospace' }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== '-O0'}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = '-O0')}
 | 
			
		||||
          >
 | 
			
		||||
            -O0
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: '$monospace' }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== '-O1'}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = '-O1')}
 | 
			
		||||
          >
 | 
			
		||||
            -O1
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: '$monospace' }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== '-O2'}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = '-O2')}
 | 
			
		||||
          >
 | 
			
		||||
            -O2
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: '$monospace' }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== '-O3'}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = '-O3')}
 | 
			
		||||
          >
 | 
			
		||||
            -O3
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: '$monospace' }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== '-O4'}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = '-O4')}
 | 
			
		||||
          >
 | 
			
		||||
            -O4
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            css={{ fontFamily: '$monospace' }}
 | 
			
		||||
            outline={snap.compileOptions.optimizationLevel !== '-Os'}
 | 
			
		||||
            onClick={() => (state.compileOptions.optimizationLevel = '-Os')}
 | 
			
		||||
          >
 | 
			
		||||
            -Os
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ButtonGroup>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Home: NextPage = () => {
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
 | 
			
		||||
  const activeFile = snap.files[snap.active] as IFile | undefined
 | 
			
		||||
  const activeFileExt = getFileExtention(activeFile?.name)
 | 
			
		||||
  const canCompile = activeFileExt === 'c' || activeFileExt === 'wat'
 | 
			
		||||
  return (
 | 
			
		||||
    <Split
 | 
			
		||||
      direction="vertical"
 | 
			
		||||
      sizes={getSplit('developVertical') || [70, 30]}
 | 
			
		||||
      minSize={[100, 100]}
 | 
			
		||||
      gutterAlign="center"
 | 
			
		||||
      gutterSize={4}
 | 
			
		||||
      style={{ height: 'calc(100vh - 60px)' }}
 | 
			
		||||
      onDragEnd={e => saveSplit('developVertical', e)}
 | 
			
		||||
    >
 | 
			
		||||
      <main style={{ display: 'flex', flex: 1, position: 'relative' }}>
 | 
			
		||||
        <HooksEditor />
 | 
			
		||||
        {canCompile && (
 | 
			
		||||
          <Hotkeys
 | 
			
		||||
            keyName="command+b,ctrl+b"
 | 
			
		||||
            onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
 | 
			
		||||
          >
 | 
			
		||||
            <Flex
 | 
			
		||||
              css={{
 | 
			
		||||
                position: 'absolute',
 | 
			
		||||
                bottom: '$4',
 | 
			
		||||
                left: '$4',
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                cursor: 'pointer',
 | 
			
		||||
                gap: '$2'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="primary"
 | 
			
		||||
                uppercase
 | 
			
		||||
                disabled={!snap.files.length}
 | 
			
		||||
                isLoading={snap.compiling}
 | 
			
		||||
                onClick={() => compileCode(snap.active)}
 | 
			
		||||
              >
 | 
			
		||||
                <Play weight="bold" size="16px" />
 | 
			
		||||
                Compile to Wasm
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Popover content={<CompilerSettings />}>
 | 
			
		||||
                <Button variant="primary" css={{ px: '10px' }}>
 | 
			
		||||
                  <Gear size="16px" />
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Popover>
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Hotkeys>
 | 
			
		||||
        )}
 | 
			
		||||
        {activeFileExt === 'js' && (
 | 
			
		||||
          <Hotkeys
 | 
			
		||||
            keyName="command+b,ctrl+b"
 | 
			
		||||
            onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
 | 
			
		||||
          >
 | 
			
		||||
            <Flex
 | 
			
		||||
              css={{
 | 
			
		||||
                position: 'absolute',
 | 
			
		||||
                bottom: '$4',
 | 
			
		||||
                left: '$4',
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                cursor: 'pointer',
 | 
			
		||||
                gap: '$2'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <RunScript file={activeFile as IFile} />
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Hotkeys>
 | 
			
		||||
        )}
 | 
			
		||||
      </main>
 | 
			
		||||
      <Flex css={{ width: '100%' }}>
 | 
			
		||||
        <Flex
 | 
			
		||||
          css={{
 | 
			
		||||
            flex: 1,
 | 
			
		||||
            background: '$mauve1',
 | 
			
		||||
            position: 'relative',
 | 
			
		||||
            borderRight: '1px solid $mauve8'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <LogBox title="Development Log" clearLog={() => (state.logs = [])} logs={snap.logs} />
 | 
			
		||||
        </Flex>
 | 
			
		||||
        {activeFileExt === 'js' && (
 | 
			
		||||
          <Flex
 | 
			
		||||
            css={{
 | 
			
		||||
              flex: 1
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <LogBox
 | 
			
		||||
              Icon={FileJs}
 | 
			
		||||
              title="Script Log"
 | 
			
		||||
              logs={snap.scriptLogs}
 | 
			
		||||
              clearLog={() => (state.scriptLogs = [])}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
        )}
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </Split>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Home
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
import type { NextPage } from "next";
 | 
			
		||||
import Head from "next/head";
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
 | 
			
		||||
import Footer from "../../components/Footer";
 | 
			
		||||
 | 
			
		||||
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const Home: NextPage = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Head>
 | 
			
		||||
        <title>XRPL Hooks Playground</title>
 | 
			
		||||
      </Head>
 | 
			
		||||
      <main style={{ display: "flex", flex: 1 }}>
 | 
			
		||||
        <HooksEditor />
 | 
			
		||||
      </main>
 | 
			
		||||
 | 
			
		||||
      <Footer />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Home;
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
const Home = () => {
 | 
			
		||||
  return <p>homepage</p>;
 | 
			
		||||
};
 | 
			
		||||
  return <p>homepage</p>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Home;
 | 
			
		||||
export default Home
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								pages/sign-in.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,37 @@
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
import { signIn, useSession } from 'next-auth/react'
 | 
			
		||||
 | 
			
		||||
import Box from '../components/Box'
 | 
			
		||||
import Spinner from '../components/Spinner'
 | 
			
		||||
 | 
			
		||||
const SignInPage = () => {
 | 
			
		||||
  const { data: session, status } = useSession()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (status !== 'loading' && !session) void signIn('github', { redirect: false })
 | 
			
		||||
    if (status !== 'loading' && session) window.close()
 | 
			
		||||
  }, [session, status])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      css={{
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        backgroundColor: '$mauve1',
 | 
			
		||||
        position: 'absolute',
 | 
			
		||||
        top: 0,
 | 
			
		||||
        right: 0,
 | 
			
		||||
        bottom: 0,
 | 
			
		||||
        left: 0,
 | 
			
		||||
        zIndex: 9999,
 | 
			
		||||
        textAlign: 'center',
 | 
			
		||||
        justifyContent: 'center',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
        gap: '$2'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      Logging in <Spinner />
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SignInPage
 | 
			
		||||
							
								
								
									
										167
									
								
								pages/test/[[...slug]].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,167 @@
 | 
			
		||||
import dynamic from 'next/dynamic'
 | 
			
		||||
import Split from 'react-split'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import { Box, Container, Flex, Tab, Tabs } from '../../components'
 | 
			
		||||
import Transaction from '../../components/Transaction'
 | 
			
		||||
import state, { renameTxState } from '../../state'
 | 
			
		||||
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
 | 
			
		||||
import { transactionsState, modifyTxState } from '../../state'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { FileJs } from 'phosphor-react'
 | 
			
		||||
import RunScript from '../../components/RunScript'
 | 
			
		||||
 | 
			
		||||
const DebugStream = dynamic(() => import('../../components/DebugStream'), {
 | 
			
		||||
  ssr: false
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const LogBox = dynamic(() => import('../../components/LogBox'), {
 | 
			
		||||
  ssr: false
 | 
			
		||||
})
 | 
			
		||||
const Accounts = dynamic(() => import('../../components/Accounts'), {
 | 
			
		||||
  ssr: false
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const Test = () => {
 | 
			
		||||
  // This and useEffect is here to prevent useLayoutEffect warnings from react-split
 | 
			
		||||
  const [showComponent, setShowComponent] = useState(false)
 | 
			
		||||
  const { transactionLogs } = useSnapshot(state)
 | 
			
		||||
  const { transactions, activeHeader } = useSnapshot(transactionsState)
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setShowComponent(true)
 | 
			
		||||
  }, [])
 | 
			
		||||
  if (!showComponent) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
  const hasScripts = Boolean(snap.files.filter(f => f.name.toLowerCase()?.endsWith('.js')).length)
 | 
			
		||||
 | 
			
		||||
  const renderNav = () => (
 | 
			
		||||
    <Flex css={{ gap: '$3' }}>
 | 
			
		||||
      {snap.files
 | 
			
		||||
        .filter(f => f.name.endsWith('.js'))
 | 
			
		||||
        .map(file => (
 | 
			
		||||
          <RunScript file={file} key={file.name} />
 | 
			
		||||
        ))}
 | 
			
		||||
    </Flex>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container css={{ px: 0 }}>
 | 
			
		||||
      <Split
 | 
			
		||||
        direction="vertical"
 | 
			
		||||
        sizes={
 | 
			
		||||
          hasScripts && getSplit('testVertical')?.length === 2
 | 
			
		||||
            ? [50, 20, 30]
 | 
			
		||||
            : hasScripts
 | 
			
		||||
            ? [50, 20, 50]
 | 
			
		||||
            : [50, 50]
 | 
			
		||||
        }
 | 
			
		||||
        gutterSize={4}
 | 
			
		||||
        gutterAlign="center"
 | 
			
		||||
        style={{ height: 'calc(100vh - 60px)' }}
 | 
			
		||||
        onDragEnd={e => saveSplit('testVertical', e)}
 | 
			
		||||
      >
 | 
			
		||||
        <Flex
 | 
			
		||||
          row
 | 
			
		||||
          fluid
 | 
			
		||||
          css={{
 | 
			
		||||
            justifyContent: 'center',
 | 
			
		||||
            p: '$3 $2'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Split
 | 
			
		||||
            direction="horizontal"
 | 
			
		||||
            sizes={getSplit('testHorizontal') || [50, 50]}
 | 
			
		||||
            minSize={[180, 320]}
 | 
			
		||||
            gutterSize={4}
 | 
			
		||||
            gutterAlign="center"
 | 
			
		||||
            style={{
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              flexDirection: 'row',
 | 
			
		||||
              width: '100%',
 | 
			
		||||
              height: '100%'
 | 
			
		||||
            }}
 | 
			
		||||
            onDragEnd={e => saveSplit('testHorizontal', e)}
 | 
			
		||||
          >
 | 
			
		||||
            <Box css={{ width: '55%', px: '$2' }}>
 | 
			
		||||
              <Tabs
 | 
			
		||||
                label="Transaction"
 | 
			
		||||
                activeHeader={activeHeader}
 | 
			
		||||
                // TODO make header a required field
 | 
			
		||||
                onChangeActive={(idx, header) => {
 | 
			
		||||
                  if (header) transactionsState.activeHeader = header
 | 
			
		||||
                }}
 | 
			
		||||
                keepAllAlive
 | 
			
		||||
                defaultExtension="json"
 | 
			
		||||
                allowedExtensions={['json']}
 | 
			
		||||
                onCreateNewTab={header => modifyTxState(header, {})}
 | 
			
		||||
                onRenameTab={(idx, nwName, oldName = '') => renameTxState(oldName, nwName)}
 | 
			
		||||
                onCloseTab={(idx, header) => header && modifyTxState(header, undefined)}
 | 
			
		||||
              >
 | 
			
		||||
                {transactions.map(({ header, state }) => (
 | 
			
		||||
                  <Tab key={header} header={header}>
 | 
			
		||||
                    <Transaction state={state} header={header} />
 | 
			
		||||
                  </Tab>
 | 
			
		||||
                ))}
 | 
			
		||||
              </Tabs>
 | 
			
		||||
            </Box>
 | 
			
		||||
            <Box css={{ width: '45%', mx: '$2', height: '100%' }}>
 | 
			
		||||
              <Accounts card hideDeployBtn showHookStats />
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Split>
 | 
			
		||||
        </Flex>
 | 
			
		||||
        {hasScripts ? (
 | 
			
		||||
          <Flex
 | 
			
		||||
            as="div"
 | 
			
		||||
            css={{
 | 
			
		||||
              borderTop: '1px solid $mauve6',
 | 
			
		||||
              background: '$mauve1',
 | 
			
		||||
              flexDirection: 'column'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <LogBox
 | 
			
		||||
              Icon={FileJs}
 | 
			
		||||
              title="Helper scripts"
 | 
			
		||||
              logs={snap.scriptLogs}
 | 
			
		||||
              clearLog={() => (state.scriptLogs = [])}
 | 
			
		||||
              renderNav={renderNav}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
        ) : null}
 | 
			
		||||
        <Flex>
 | 
			
		||||
          <Split
 | 
			
		||||
            direction="horizontal"
 | 
			
		||||
            sizes={[50, 50]}
 | 
			
		||||
            minSize={[320, 160]}
 | 
			
		||||
            gutterSize={4}
 | 
			
		||||
            gutterAlign="center"
 | 
			
		||||
            style={{
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              flexDirection: 'row',
 | 
			
		||||
              width: '100%',
 | 
			
		||||
              height: '100%'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Box
 | 
			
		||||
              css={{
 | 
			
		||||
                borderRight: '1px solid $mauve8',
 | 
			
		||||
                height: '100%'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <LogBox
 | 
			
		||||
                title="Development Log"
 | 
			
		||||
                logs={transactionLogs}
 | 
			
		||||
                clearLog={() => (state.transactionLogs = [])}
 | 
			
		||||
              />
 | 
			
		||||
            </Box>
 | 
			
		||||
            <Box css={{ height: '100%' }}>
 | 
			
		||||
              <DebugStream />
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Split>
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </Split>
 | 
			
		||||
    </Container>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Test
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
import Container from "../../components/Container";
 | 
			
		||||
 | 
			
		||||
const Test = () => {
 | 
			
		||||
  return <Container css={{ py: "$10" }}>This will be the test page</Container>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Test;
 | 
			
		||||
							
								
								
									
										3304
									
								
								patches/ripple-binary-codec+1.4.2.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/android-chrome-192x192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.0 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/android-chrome-512x512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										9
									
								
								public/browserconfig.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<browserconfig>
 | 
			
		||||
    <msapplication>
 | 
			
		||||
        <tile>
 | 
			
		||||
            <square150x150logo src="/mstile-150x150.png"/>
 | 
			
		||||
            <TileColor>#161618</TileColor>
 | 
			
		||||
        </tile>
 | 
			
		||||
    </msapplication>
 | 
			
		||||
</browserconfig>
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 737 B  | 
							
								
								
									
										
											BIN
										
									
								
								public/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.1 KiB  | 
| 
		 Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/mstile-150x150.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										9
									
								
								public/pattern-2.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 138 KiB  | 
							
								
								
									
										9
									
								
								public/pattern-dark-2.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 152 KiB  | 
							
								
								
									
										9
									
								
								public/pattern-dark.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 153 KiB  | 
							
								
								
									
										9
									
								
								public/pattern.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 134 KiB  | 
							
								
								
									
										18
									
								
								public/safari-pinned-tab.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
<?xml version="1.0" standalone="no"?>
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
 | 
			
		||||
 "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
 | 
			
		||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
 width="588.000000pt" height="588.000000pt" viewBox="0 0 588.000000 588.000000"
 | 
			
		||||
 preserveAspectRatio="xMidYMid meet">
 | 
			
		||||
<metadata>
 | 
			
		||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
 | 
			
		||||
</metadata>
 | 
			
		||||
<g transform="translate(0.000000,588.000000) scale(0.100000,-0.100000)"
 | 
			
		||||
fill="#000000" stroke="none">
 | 
			
		||||
<path d="M3843 4097 c-381 -380 -737 -736 -791 -790 l-99 -99 -940 1 -940 0
 | 
			
		||||
-204 -211 c-112 -116 -228 -235 -257 -265 -29 -30 -51 -57 -48 -60 2 -3 542
 | 
			
		||||
-5 1198 -5 l1193 0 799 -799 799 -799 380 0 c209 0 378 2 376 5 -2 2 -422 423
 | 
			
		||||
-933 934 l-928 929 921 920 c507 507 921 923 921 927 0 3 -170 5 -377 5 l-378
 | 
			
		||||
0 -692 -693z"/>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 837 B  | 
							
								
								
									
										
											BIN
										
									
								
								public/share-image.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 710 KiB  | 
							
								
								
									
										19
									
								
								public/site.webmanifest
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "Hooks Builder",
 | 
			
		||||
  "short_name": "Hooks Builder",
 | 
			
		||||
  "icons": [
 | 
			
		||||
    {
 | 
			
		||||
      "src": "/android-chrome-192x192.png",
 | 
			
		||||
      "sizes": "192x192",
 | 
			
		||||
      "type": "image/png"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "src": "/android-chrome-512x512.png",
 | 
			
		||||
      "sizes": "512x512",
 | 
			
		||||
      "type": "image/png"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "theme_color": "#161618",
 | 
			
		||||
  "background_color": "#161618",
 | 
			
		||||
  "display": "standalone"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none" 
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										9
									
								
								raw-loader.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
declare module '*.md' {
 | 
			
		||||
  const content: string
 | 
			
		||||
  export default content
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module '*.hook-bundle.js' {
 | 
			
		||||
  const content: string
 | 
			
		||||
  export default content
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										152
									
								
								state.ts
									
									
									
									
									
								
							
							
						
						@@ -1,152 +0,0 @@
 | 
			
		||||
import { proxy, subscribe } from 'valtio';
 | 
			
		||||
import { devtools } from 'valtio/utils';
 | 
			
		||||
import { Octokit } from '@octokit/core';
 | 
			
		||||
import type monaco from 'monaco-editor';
 | 
			
		||||
import toast from 'react-hot-toast';
 | 
			
		||||
 | 
			
		||||
const octokit = new Octokit();
 | 
			
		||||
 | 
			
		||||
interface File {
 | 
			
		||||
  name: string,
 | 
			
		||||
  language: string,
 | 
			
		||||
  content: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
  files: File[],
 | 
			
		||||
  active: number;
 | 
			
		||||
  loading: boolean;
 | 
			
		||||
  compiling: boolean;
 | 
			
		||||
  logs: {
 | 
			
		||||
    type: 'error' | 'warning' | 'log',
 | 
			
		||||
    message: string;
 | 
			
		||||
  }[];
 | 
			
		||||
  editorCtx?: typeof monaco.editor;
 | 
			
		||||
  editorSettings: {
 | 
			
		||||
    tabSize: number;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let localStorageState: null | string = null;
 | 
			
		||||
let initialState = {
 | 
			
		||||
  files: [],
 | 
			
		||||
  active: 0,
 | 
			
		||||
  loading: false,
 | 
			
		||||
  compiling: false,
 | 
			
		||||
  logs: [],
 | 
			
		||||
  editorCtx: undefined,
 | 
			
		||||
  editorSettings: {
 | 
			
		||||
    tabSize: 2
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Check if there's a persited state in localStorage
 | 
			
		||||
if (typeof window !== 'undefined') {
 | 
			
		||||
  try {
 | 
			
		||||
    localStorageState = localStorage.getItem('hooksIdeState');
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.log(`localStorage state broken`);
 | 
			
		||||
    localStorage.removeItem('hooksIdeState');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
if (localStorageState) {
 | 
			
		||||
  initialState = JSON.parse(localStorageState);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Initialize state
 | 
			
		||||
export const state = proxy<IState>(initialState);
 | 
			
		||||
 | 
			
		||||
// Fetch content from Githug Gists
 | 
			
		||||
export const fetchFiles = (gistId: string) => {
 | 
			
		||||
  if (gistId) {
 | 
			
		||||
    state.logs.push({ type: 'log', message: `Fetching Gist with id: ${gistId}` });
 | 
			
		||||
    octokit.request("GET /gists/{gist_id}", { gist_id: gistId }).then(res => {
 | 
			
		||||
      if (res.data.files && Object.keys(res.data.files).length > 0) {
 | 
			
		||||
        const files = Object.keys(res.data.files).map(filename => ({
 | 
			
		||||
          name: res.data.files?.[filename]?.filename || 'noname.c',
 | 
			
		||||
          language: res.data.files?.[filename]?.language?.toLowerCase() || '',
 | 
			
		||||
          content: res.data.files?.[filename]?.content || ''
 | 
			
		||||
        }))
 | 
			
		||||
        state.loading = false;
 | 
			
		||||
        if (files.length > 0) {
 | 
			
		||||
          state.logs.push({ type: 'log', message: 'Fetched successfully ✅' })
 | 
			
		||||
          state.files = files;
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    }).catch(err => {
 | 
			
		||||
      state.loading = false;
 | 
			
		||||
      state.logs.push({ type: 'error', message: `Couldn't find Gist with id: ${gistId}` })
 | 
			
		||||
      return
 | 
			
		||||
    })
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  state.loading = false;
 | 
			
		||||
  // return state.files = initFiles
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const updateEditorSettings = (editorSettings: IState['editorSettings']) => {
 | 
			
		||||
  state.editorCtx?.getModels().forEach(model => {
 | 
			
		||||
    console.log(model.uri)
 | 
			
		||||
    model.updateOptions({
 | 
			
		||||
      ...editorSettings
 | 
			
		||||
    })
 | 
			
		||||
  });
 | 
			
		||||
  return state.editorSettings = editorSettings;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const saveFile = (value: string) => {
 | 
			
		||||
  toast.success('Saved successfully', { position: 'bottom-center' })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const createNewFile = (name: string) => {
 | 
			
		||||
  state.files.push({ name, language: 'c', content: "" })
 | 
			
		||||
  state.active = state.files.length - 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const compileCode = async (activeId: number) => {
 | 
			
		||||
  if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
 | 
			
		||||
    throw Error('Missing env!')
 | 
			
		||||
  };
 | 
			
		||||
  if (state.compiling) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  state.compiling = true;
 | 
			
		||||
  try {
 | 
			
		||||
    const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      headers: {
 | 
			
		||||
        'Content-Type': 'application/json'
 | 
			
		||||
      },
 | 
			
		||||
      body: JSON.stringify({
 | 
			
		||||
        "output": "wasm",
 | 
			
		||||
        "compress": true,
 | 
			
		||||
        "files": [
 | 
			
		||||
          {
 | 
			
		||||
            "type": "c",
 | 
			
		||||
            "name": state.files[activeId].name,
 | 
			
		||||
            "options": "-g -O3",
 | 
			
		||||
            "src": state.files[activeId].content
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      })
 | 
			
		||||
    });
 | 
			
		||||
    const json = await res.json();
 | 
			
		||||
    state.compiling = false;
 | 
			
		||||
    toast.success('Compiled successfully!');
 | 
			
		||||
    console.log(json)
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.log(err)
 | 
			
		||||
    state.logs.push({ type: 'error', message: 'Error occured while compiling!' })
 | 
			
		||||
    state.compiling = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const unsub = devtools(state, 'Files State');
 | 
			
		||||
 | 
			
		||||
subscribe(state, () => {
 | 
			
		||||
  const { editorCtx, ...storedState } = state;
 | 
			
		||||
  localStorage.setItem('hooksIdeState', JSON.stringify(storedState))
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										86
									
								
								state/actions/addFaucetAccount.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,86 @@
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import state, { FaucetAccountRes } from '../index'
 | 
			
		||||
import fetchAccountInfo from '../../utils/accountInfo';
 | 
			
		||||
 | 
			
		||||
export const names = [
 | 
			
		||||
  'Alice',
 | 
			
		||||
  'Bob',
 | 
			
		||||
  'Carol',
 | 
			
		||||
  'Carlos',
 | 
			
		||||
  'Charlie',
 | 
			
		||||
  'Dan',
 | 
			
		||||
  'Dave',
 | 
			
		||||
  'David',
 | 
			
		||||
  'Faythe',
 | 
			
		||||
  'Frank',
 | 
			
		||||
  'Grace',
 | 
			
		||||
  'Heidi',
 | 
			
		||||
  'Judy',
 | 
			
		||||
  'Olive',
 | 
			
		||||
  'Peggy',
 | 
			
		||||
  'Walter'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
/* This function adds faucet account to application global state.
 | 
			
		||||
 * It calls the /api/faucet endpoint which in send a HTTP POST to
 | 
			
		||||
 * https://hooks-testnet.xrpl-labs.com/newcreds and it returns
 | 
			
		||||
 * new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
 | 
			
		||||
 * is protected with CORS so that's why we did our own endpoint
 | 
			
		||||
 */
 | 
			
		||||
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
 | 
			
		||||
  if (typeof window === undefined) return
 | 
			
		||||
 | 
			
		||||
  const toastId = showToast ? toast.loading('Creating account') : ''
 | 
			
		||||
  const res = await fetch(`${window.location.origin}/api/faucet`, {
 | 
			
		||||
    method: 'POST'
 | 
			
		||||
  })
 | 
			
		||||
  const json: FaucetAccountRes | { error: string } = await res.json()
 | 
			
		||||
  if ('error' in json) {
 | 
			
		||||
    if (!showToast) return;
 | 
			
		||||
    return toast.error(json.error, { id: toastId })
 | 
			
		||||
  }
 | 
			
		||||
  const currNames = state.accounts.map(acc => acc.name)
 | 
			
		||||
  const info = await fetchAccountInfo(json.address, { silent: true })
 | 
			
		||||
  state.accounts.push({
 | 
			
		||||
    name: name || names.filter(name => !currNames.includes(name))[0],
 | 
			
		||||
    xrp: (json.xrp || 0 * 1000000).toString(),
 | 
			
		||||
    address: json.address,
 | 
			
		||||
    secret: json.secret,
 | 
			
		||||
    sequence: info?.Sequence || 1,
 | 
			
		||||
    hooks: [],
 | 
			
		||||
    isLoading: false,
 | 
			
		||||
    version: '2'
 | 
			
		||||
  })
 | 
			
		||||
  if (showToast) {
 | 
			
		||||
    toast.success('New account created', { id: toastId })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  // fetch initial faucets
 | 
			
		||||
  ; (async function fetchFaucets() {
 | 
			
		||||
    if (typeof window !== 'undefined') {
 | 
			
		||||
      if (state.accounts.length === 0) {
 | 
			
		||||
        await addFaucetAccount()
 | 
			
		||||
        // setTimeout(() => {
 | 
			
		||||
        //   addFaucetAccount();
 | 
			
		||||
        // }, 10000);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })()
 | 
			
		||||
 | 
			
		||||
export const addFunds = async (address: string) => {
 | 
			
		||||
  const toastId = toast.loading('Requesting funds')
 | 
			
		||||
  const res = await fetch(`${window.location.origin}/api/faucet?account=${address}`, {
 | 
			
		||||
    method: 'POST'
 | 
			
		||||
  })
 | 
			
		||||
  const json: FaucetAccountRes | { error: string } = await res.json()
 | 
			
		||||
  if ('error' in json) {
 | 
			
		||||
    return toast.error(json.error, { id: toastId })
 | 
			
		||||
  } else {
 | 
			
		||||
    toast.success(`Funds added (${json.xrp} XRP)`, { id: toastId })
 | 
			
		||||
    const currAccount = state.accounts.find(acc => acc.address === address)
 | 
			
		||||
    if (currAccount) {
 | 
			
		||||
      currAccount.xrp = (Number(currAccount.xrp) + json.xrp * 1000000).toString()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										171
									
								
								state/actions/compileCode.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,171 @@
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import Router from 'next/router'
 | 
			
		||||
 | 
			
		||||
import state from '../index'
 | 
			
		||||
import { saveFile } from './saveFile'
 | 
			
		||||
import { decodeBinary } from '../../utils/decodeBinary'
 | 
			
		||||
import { ref } from 'valtio'
 | 
			
		||||
 | 
			
		||||
/* compileCode sends the code of the active file to compile endpoint
 | 
			
		||||
 * If all goes well you will get base64 encoded wasm file back with
 | 
			
		||||
 * some extra logging information if we can provide it. This function
 | 
			
		||||
 * also decodes the returned wasm and creates human readable WAT file
 | 
			
		||||
 * out of it and store both in global state.
 | 
			
		||||
 */
 | 
			
		||||
export const compileCode = async (activeId: number) => {
 | 
			
		||||
  // Save the file to global state
 | 
			
		||||
  saveFile(false, activeId)
 | 
			
		||||
  const file = state.files[activeId]
 | 
			
		||||
  if (file.name.endsWith('.wat')) {
 | 
			
		||||
    return compileWat(activeId)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
 | 
			
		||||
    throw Error('Missing env!')
 | 
			
		||||
  }
 | 
			
		||||
  // Bail out if we're already compiling
 | 
			
		||||
  if (state.compiling) {
 | 
			
		||||
    // if compiling is ongoing return // TODO Inform user about it.
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  // Set loading state to true
 | 
			
		||||
  state.compiling = true
 | 
			
		||||
  state.logs = []
 | 
			
		||||
  try {
 | 
			
		||||
    file.containsErrors = false
 | 
			
		||||
    let res: Response
 | 
			
		||||
    try {
 | 
			
		||||
      res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({
 | 
			
		||||
          output: 'wasm',
 | 
			
		||||
          compress: true,
 | 
			
		||||
          strip: state.compileOptions.strip,
 | 
			
		||||
          files: [
 | 
			
		||||
            {
 | 
			
		||||
              type: 'c',
 | 
			
		||||
              options: state.compileOptions.optimizationLevel || '-O2',
 | 
			
		||||
              name: file.name,
 | 
			
		||||
              src: file.content
 | 
			
		||||
            }
 | 
			
		||||
          ]
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw Error('Something went wrong, check your network connection and try again!')
 | 
			
		||||
    }
 | 
			
		||||
    const json = await res.json()
 | 
			
		||||
    state.compiling = false
 | 
			
		||||
    if (!json.success) {
 | 
			
		||||
      const errors = [json.message]
 | 
			
		||||
      if (json.tasks && json.tasks.length > 0) {
 | 
			
		||||
        json.tasks.forEach((task: any) => {
 | 
			
		||||
          if (!task.success) {
 | 
			
		||||
            errors.push(task?.console)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      throw errors
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      // Decode base64 encoded wasm that is coming back from the endpoint
 | 
			
		||||
      const bufferData = await decodeBinary(json.output)
 | 
			
		||||
 | 
			
		||||
      // Import wabt from and create human readable version of wasm file and
 | 
			
		||||
      // put it into state
 | 
			
		||||
      const ww = await (await import('wabt')).default()
 | 
			
		||||
      const myModule = ww.readWasm(new Uint8Array(bufferData), {
 | 
			
		||||
        readDebugNames: true
 | 
			
		||||
      })
 | 
			
		||||
      myModule.applyNames()
 | 
			
		||||
 | 
			
		||||
      const wast = myModule.toText({ foldExprs: false, inlineExport: false })
 | 
			
		||||
 | 
			
		||||
      file.compiledContent = ref(bufferData)
 | 
			
		||||
      file.lastCompiled = new Date()
 | 
			
		||||
      file.compiledValueSnapshot = file.content
 | 
			
		||||
      file.compiledWatContent = wast
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw Error('Invalid compilation result produced, check your code for errors and try again!')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toast.success('Compiled successfully!', { position: 'bottom-center' })
 | 
			
		||||
    state.logs.push({
 | 
			
		||||
      type: 'success',
 | 
			
		||||
      message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
 | 
			
		||||
      link: Router.asPath.replace('develop', 'deploy'),
 | 
			
		||||
      linkText: 'Go to deploy'
 | 
			
		||||
    })
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.log(err)
 | 
			
		||||
 | 
			
		||||
    if (err instanceof Array && typeof err[0] === 'string') {
 | 
			
		||||
      err.forEach(message => {
 | 
			
		||||
        state.logs.push({
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          message
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    } else if (err instanceof Error) {
 | 
			
		||||
      state.logs.push({
 | 
			
		||||
        type: 'error',
 | 
			
		||||
        message: err.message
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      state.logs.push({
 | 
			
		||||
        type: 'error',
 | 
			
		||||
        message: 'Something went wrong, come back later!'
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state.compiling = false
 | 
			
		||||
    toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
 | 
			
		||||
    file.containsErrors = true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const compileWat = async (activeId: number) => {
 | 
			
		||||
  if (state.compiling) return;
 | 
			
		||||
  const file = state.files[activeId]
 | 
			
		||||
  state.compiling = true
 | 
			
		||||
  state.logs = []
 | 
			
		||||
  try {
 | 
			
		||||
    const wabt = await (await import('wabt')).default()
 | 
			
		||||
    const module = wabt.parseWat(file.name, file.content);
 | 
			
		||||
    module.resolveNames();
 | 
			
		||||
    module.validate();
 | 
			
		||||
    const { buffer } = module.toBinary({
 | 
			
		||||
      log: false,
 | 
			
		||||
      write_debug_names: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    file.compiledContent = ref(buffer)
 | 
			
		||||
    file.lastCompiled = new Date()
 | 
			
		||||
    file.compiledValueSnapshot = file.content
 | 
			
		||||
    file.compiledWatContent = file.content
 | 
			
		||||
 | 
			
		||||
    toast.success('Compiled successfully!', { position: 'bottom-center' })
 | 
			
		||||
    state.logs.push({
 | 
			
		||||
      type: 'success',
 | 
			
		||||
      message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
 | 
			
		||||
      link: Router.asPath.replace('develop', 'deploy'),
 | 
			
		||||
      linkText: 'Go to deploy'
 | 
			
		||||
    })
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.log(err)
 | 
			
		||||
    let message = "Error compiling WAT file!"
 | 
			
		||||
    if (err instanceof Error) {
 | 
			
		||||
      message = err.message
 | 
			
		||||
    }
 | 
			
		||||
    state.logs.push({
 | 
			
		||||
      type: 'error',
 | 
			
		||||
      message
 | 
			
		||||
    })
 | 
			
		||||
    toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
 | 
			
		||||
    file.containsErrors = true
 | 
			
		||||
  }
 | 
			
		||||
  state.compiling = false
 | 
			
		||||
}
 | 
			
		||||