Compare commits
	
		
			139 Commits
		
	
	
		
			feature/em
			...
			feat/persi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					810d3b2524 | ||
| 
						 | 
					a3393ded1e | ||
| 
						 | 
					17ede265b1 | ||
| 
						 | 
					629070edad | ||
| 
						 | 
					cc83924c27 | ||
| 
						 | 
					e3e964f72a | ||
| 
						 | 
					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 | 
@@ -1,3 +1,5 @@
 | 
			
		||||
NEXTAUTH_URL=https://example.com
 | 
			
		||||
GITHUB_SECRET=""
 | 
			
		||||
GITHUB_ID=""
 | 
			
		||||
GITHUB_ID=""
 | 
			
		||||
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
 | 
			
		||||
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
 | 
			
		||||
							
								
								
									
										79
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -8,7 +8,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 +26,75 @@ 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.
 | 
			
		||||
 | 
			
		||||
## 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:
 | 
			
		||||
@@ -32,9 +103,3 @@ To learn more about Next.js, take a look at the following resources:
 | 
			
		||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										475
									
								
								components/Accounts.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,475 @@
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import { ArrowSquareOut, Copy, Wallet, X } from "phosphor-react";
 | 
			
		||||
import React, { useEffect, useState, FC } from "react";
 | 
			
		||||
import Dinero from "dinero.js";
 | 
			
		||||
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import { addFaucetAccount, deployHook, 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 } from "./Input";
 | 
			
		||||
 | 
			
		||||
const labelStyle = css({
 | 
			
		||||
  color: "$mauve10",
 | 
			
		||||
  textTransform: "uppercase",
 | 
			
		||||
  fontSize: "10px",
 | 
			
		||||
  mb: "$0.5",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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}
 | 
			
		||||
        </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",
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {activeAccount?.address}
 | 
			
		||||
                </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",
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {Dinero({
 | 
			
		||||
                    amount: Number(activeAccount?.xrp || "0"),
 | 
			
		||||
                    precision: 6,
 | 
			
		||||
                  })
 | 
			
		||||
                    .toUnit()
 | 
			
		||||
                    .toLocaleString(undefined, {
 | 
			
		||||
                      style: "currency",
 | 
			
		||||
                      currency: "XRP",
 | 
			
		||||
                      currencyDisplay: "name",
 | 
			
		||||
                    })}
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Flex>
 | 
			
		||||
              <Flex css={{ marginLeft: "auto" }}>
 | 
			
		||||
                <a
 | 
			
		||||
                  href={`https://hooks-testnet-explorer.xrpl-labs.com/${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",
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {activeAccount && activeAccount.hooks.length}
 | 
			
		||||
                </Text>
 | 
			
		||||
              </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 =>
 | 
			
		||||
          snap.client?.send({
 | 
			
		||||
            id: 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;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        const objectRequests = snap.accounts.map(acc => {
 | 
			
		||||
          return snap.client?.send({
 | 
			
		||||
            id: `${acc.address}-hooks`,
 | 
			
		||||
            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
 | 
			
		||||
              .filter((ac: any) => ac?.LedgerEntryType === "Hook")
 | 
			
		||||
              .map((oo: any) => oo.HookHash);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let fetchAccountInfoInterval: NodeJS.Timer;
 | 
			
		||||
    if (snap.clientStatus === "online") {
 | 
			
		||||
      fetchAccInfo();
 | 
			
		||||
      fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 2000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (snap.accounts.length > 0) {
 | 
			
		||||
        if (fetchAccountInfoInterval) {
 | 
			
		||||
          clearInterval(fetchAccountInfoInterval);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [snap.accounts, 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" }}>
 | 
			
		||||
            <Button ghost size="sm" onClick={() => addFaucetAccount(true)}>
 | 
			
		||||
              Create
 | 
			
		||||
            </Button>
 | 
			
		||||
            <ImportAccountDialog />
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </Flex>
 | 
			
		||||
        <Stack
 | 
			
		||||
          css={{
 | 
			
		||||
            flexDirection: "column",
 | 
			
		||||
            width: "100%",
 | 
			
		||||
            fontSize: "13px",
 | 
			
		||||
            wordWrap: "break-word",
 | 
			
		||||
            fontWeight: "$body",
 | 
			
		||||
            fontFamily: "$monospace",
 | 
			
		||||
            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: "$mauve9",
 | 
			
		||||
                      wordBreak: "break-word",
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    {account.address} (
 | 
			
		||||
                    {Dinero({
 | 
			
		||||
                      amount: Number(account?.xrp || "0"),
 | 
			
		||||
                      precision: 6,
 | 
			
		||||
                    })
 | 
			
		||||
                      .toUnit()
 | 
			
		||||
                      .toLocaleString(undefined, {
 | 
			
		||||
                        style: "currency",
 | 
			
		||||
                        currency: "XRP",
 | 
			
		||||
                        currencyDisplay: "name",
 | 
			
		||||
                      })}
 | 
			
		||||
                    )
 | 
			
		||||
                  </Text>
 | 
			
		||||
                </Box>
 | 
			
		||||
                {!props.hideDeployBtn && (
 | 
			
		||||
                  <Button
 | 
			
		||||
                    css={{ ml: "auto" }}
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    uppercase
 | 
			
		||||
                    isLoading={account.isLoading}
 | 
			
		||||
                    disabled={
 | 
			
		||||
                      account.isLoading ||
 | 
			
		||||
                      !snap.files.filter(file => file.compiledWatContent).length
 | 
			
		||||
                    }
 | 
			
		||||
                    variant="secondary"
 | 
			
		||||
                    onClick={e => {
 | 
			
		||||
                      e.stopPropagation();
 | 
			
		||||
                      deployHook(account);
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Deploy
 | 
			
		||||
                  </Button>
 | 
			
		||||
                )}
 | 
			
		||||
              </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>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ImportAccountDialog = () => {
 | 
			
		||||
  const [value, setValue] = useState("");
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog>
 | 
			
		||||
      <DialogTrigger asChild>
 | 
			
		||||
        <Button ghost size="sm">
 | 
			
		||||
          Import
 | 
			
		||||
        </Button>
 | 
			
		||||
      </DialogTrigger>
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <DialogTitle>Import account</DialogTitle>
 | 
			
		||||
        <DialogDescription>
 | 
			
		||||
          <label>Add account secret</label>
 | 
			
		||||
          <Input
 | 
			
		||||
            name="secret"
 | 
			
		||||
            type="password"
 | 
			
		||||
            value={value}
 | 
			
		||||
            onChange={e => setValue(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={() => {
 | 
			
		||||
                importAccount(value);
 | 
			
		||||
                setValue("");
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Import account
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
        </Flex>
 | 
			
		||||
        <DialogClose asChild>
 | 
			
		||||
          <Box css={{ position: "absolute", top: "$3", right: "$3" }}>
 | 
			
		||||
            <X size="20px" />
 | 
			
		||||
          </Box>
 | 
			
		||||
        </DialogClose>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Accounts;
 | 
			
		||||
							
								
								
									
										88
									
								
								components/AlertDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,88 @@
 | 
			
		||||
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: "$sm",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 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,12 @@
 | 
			
		||||
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",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  appereance: "none",
 | 
			
		||||
  fontFamily: "$body",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
@@ -27,14 +31,21 @@ const Button = styled("button", {
 | 
			
		||||
  fontSize: "$2",
 | 
			
		||||
  fontWeight: 500,
 | 
			
		||||
  fontVariantNumeric: "tabular-nums",
 | 
			
		||||
  backgroundColor: "red",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  width: "max-content",
 | 
			
		||||
  "&:disabled": {
 | 
			
		||||
    opacity: 0.8,
 | 
			
		||||
    opacity: 0.6,
 | 
			
		||||
    pointerEvents: "none",
 | 
			
		||||
    cursor: "not-allowed",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    size: {
 | 
			
		||||
      xs: {
 | 
			
		||||
        borderRadius: "$sm",
 | 
			
		||||
        height: "$5",
 | 
			
		||||
        px: "$2",
 | 
			
		||||
        fontSize: "$xs",
 | 
			
		||||
      },
 | 
			
		||||
      sm: {
 | 
			
		||||
        borderRadius: "$sm",
 | 
			
		||||
        height: "$7",
 | 
			
		||||
@@ -56,53 +67,77 @@ const Button = styled("button", {
 | 
			
		||||
    },
 | 
			
		||||
    variant: {
 | 
			
		||||
      default: {
 | 
			
		||||
        backgroundColor: "$slate12",
 | 
			
		||||
        boxShadow: "inset 0 0 0 1px $colors$slate12",
 | 
			
		||||
        color: "$slate1",
 | 
			
		||||
        backgroundColor: "$mauve12",
 | 
			
		||||
        boxShadow: "inset 0 0 0 1px $colors$mauve12",
 | 
			
		||||
        color: "$mauve1",
 | 
			
		||||
        "@hover": {
 | 
			
		||||
          "&:hover": {
 | 
			
		||||
            backgroundColor: "$slate12",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$slate12",
 | 
			
		||||
            backgroundColor: "$mauve12",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$mauve12",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        "&:active": {
 | 
			
		||||
          backgroundColor: "$slate10",
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$slate11",
 | 
			
		||||
          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",
 | 
			
		||||
            "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",
 | 
			
		||||
        backgroundColor: `$accent`,
 | 
			
		||||
        boxShadow: "inset 0 0 0 1px $colors$purple9",
 | 
			
		||||
        color: "$white",
 | 
			
		||||
        "@hover": {
 | 
			
		||||
          "&:hover": {
 | 
			
		||||
            backgroundColor: "$pink10",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$pink11",
 | 
			
		||||
            backgroundColor: "$purple10",
 | 
			
		||||
            boxShadow: "inset 0 0 0 1px $colors$purple11",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        "&:active": {
 | 
			
		||||
          backgroundColor: "$pink8",
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$pink8",
 | 
			
		||||
          backgroundColor: "$purple8",
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$purple8",
 | 
			
		||||
        },
 | 
			
		||||
        "&:focus": {
 | 
			
		||||
          boxShadow: "inset 0 0 0 1px $colors$pink8",
 | 
			
		||||
          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",
 | 
			
		||||
          },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    outline: {
 | 
			
		||||
      true: {
 | 
			
		||||
        backgroundColor: "transparent",
 | 
			
		||||
@@ -113,19 +148,24 @@ const Button = styled("button", {
 | 
			
		||||
        textTransform: "uppercase",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    fullWidth: {
 | 
			
		||||
      true: {
 | 
			
		||||
        width: "100%",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    ghost: {
 | 
			
		||||
      true: {
 | 
			
		||||
        boxShadow: "none",
 | 
			
		||||
        background: "transparent",
 | 
			
		||||
        color: "$slate12",
 | 
			
		||||
        color: "$mauve12",
 | 
			
		||||
        "@hover": {
 | 
			
		||||
          "&:hover": {
 | 
			
		||||
            backgroundColor: "$slate6",
 | 
			
		||||
            backgroundColor: "$mauve6",
 | 
			
		||||
            boxShadow: "none",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        "&:active": {
 | 
			
		||||
          backgroundColor: "$slate8",
 | 
			
		||||
          backgroundColor: "$mauve8",
 | 
			
		||||
          boxShadow: "none",
 | 
			
		||||
        },
 | 
			
		||||
        "&:focus": {
 | 
			
		||||
@@ -133,19 +173,26 @@ const Button = styled("button", {
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    isLoading: {
 | 
			
		||||
      true: {
 | 
			
		||||
        "& .button-content": {
 | 
			
		||||
          visibility: "hidden",
 | 
			
		||||
        },
 | 
			
		||||
        pointerEvents: "none",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  compoundVariants: [
 | 
			
		||||
    {
 | 
			
		||||
      outline: true,
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      css: {
 | 
			
		||||
        background: "transparent",
 | 
			
		||||
        color: "$slate12",
 | 
			
		||||
        boxShadow: "inset 0 0 0 1px $colors$slate10",
 | 
			
		||||
        color: "$mauve12",
 | 
			
		||||
        boxShadow: "inset 0 0 0 1px $colors$mauve10",
 | 
			
		||||
        "&:hover": {
 | 
			
		||||
          color: "$slate12",
 | 
			
		||||
          background: "$slate5",
 | 
			
		||||
          color: "$mauve12",
 | 
			
		||||
          background: "$mauve5",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
@@ -154,10 +201,22 @@ const Button = styled("button", {
 | 
			
		||||
      variant: "primary",
 | 
			
		||||
      css: {
 | 
			
		||||
        background: "transparent",
 | 
			
		||||
        color: "$slate12",
 | 
			
		||||
        color: "$mauve12",
 | 
			
		||||
        "&:hover": {
 | 
			
		||||
          color: "$slate12",
 | 
			
		||||
          background: "$slate5",
 | 
			
		||||
          color: "$mauve12",
 | 
			
		||||
          background: "$mauve5",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      outline: true,
 | 
			
		||||
      variant: "secondary",
 | 
			
		||||
      css: {
 | 
			
		||||
        background: "transparent",
 | 
			
		||||
        color: "$mauve12",
 | 
			
		||||
        "&:hover": {
 | 
			
		||||
          color: "$mauve12",
 | 
			
		||||
          background: "$mauve5",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
@@ -168,4 +227,22 @@ const Button = styled("button", {
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										86
									
								
								components/DebugStream.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,86 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import { Select } from ".";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
import LogBox from "./LogBox";
 | 
			
		||||
import Text from "./Text";
 | 
			
		||||
 | 
			
		||||
const DebugStream = () => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
 | 
			
		||||
  const accountOptions = snap.accounts.map(acc => ({
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
    value: acc.address,
 | 
			
		||||
  }));
 | 
			
		||||
  const [selectedAccount, setSelectedAccount] = useState<typeof accountOptions[0] | null>(null);
 | 
			
		||||
 | 
			
		||||
  const renderNav = () => (
 | 
			
		||||
    <>
 | 
			
		||||
      <Text css={{ mx: "$2", fontSize: "inherit" }}>Account: </Text>
 | 
			
		||||
      <Select
 | 
			
		||||
        instanceId="debugStreamAccount"
 | 
			
		||||
        placeholder="Select account"
 | 
			
		||||
        options={accountOptions}
 | 
			
		||||
        hideSelectedOptions
 | 
			
		||||
        value={selectedAccount}
 | 
			
		||||
        onChange={acc => setSelectedAccount(acc as any)}
 | 
			
		||||
        css={{ width: "30%" }}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const account = selectedAccount?.value;
 | 
			
		||||
    if (!account) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const socket = new WebSocket(`wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`);
 | 
			
		||||
 | 
			
		||||
    const onOpen = () => {
 | 
			
		||||
      state.debugLogs = [];
 | 
			
		||||
      state.debugLogs.push({
 | 
			
		||||
        type: "success",
 | 
			
		||||
        message: `Debug stream opened for account ${account}`,
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
    const onError = () => {
 | 
			
		||||
      state.debugLogs.push({
 | 
			
		||||
        type: "error",
 | 
			
		||||
        message: "Something went wrong in establishing connection!",
 | 
			
		||||
      });
 | 
			
		||||
      setSelectedAccount(null);
 | 
			
		||||
    };
 | 
			
		||||
    const onMessage = (event: any) => {
 | 
			
		||||
      if (!event.data) return;
 | 
			
		||||
      state.debugLogs.push({
 | 
			
		||||
        type: "log",
 | 
			
		||||
        message: event.data,
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    socket.addEventListener("open", onOpen);
 | 
			
		||||
    socket.addEventListener("close", onError);
 | 
			
		||||
    socket.addEventListener("error", onError);
 | 
			
		||||
    socket.addEventListener("message", onMessage);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      socket.removeEventListener("open", onOpen);
 | 
			
		||||
      socket.removeEventListener("close", onError);
 | 
			
		||||
      socket.removeEventListener("message", onMessage);
 | 
			
		||||
 | 
			
		||||
      socket.close();
 | 
			
		||||
    };
 | 
			
		||||
  }, [selectedAccount]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <LogBox
 | 
			
		||||
      enhanced
 | 
			
		||||
      renderNav={renderNav}
 | 
			
		||||
      title="Debug stream"
 | 
			
		||||
      logs={snap.debugLogs}
 | 
			
		||||
      clearLog={() => (state.debugLogs = [])}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DebugStream;
 | 
			
		||||
							
								
								
									
										100
									
								
								components/DeployEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,100 @@
 | 
			
		||||
import React, { useRef } from "react";
 | 
			
		||||
import { useSnapshot, ref } from "valtio";
 | 
			
		||||
import Editor, { loader } from "@monaco-editor/react";
 | 
			
		||||
import type monaco from "monaco-editor";
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import NextLink from "next/link";
 | 
			
		||||
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import dark from "../theme/editor/amy.json";
 | 
			
		||||
import light from "../theme/editor/xcode_default.json";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
 | 
			
		||||
import EditorNavigation from "./EditorNavigation";
 | 
			
		||||
import Text from "./Text";
 | 
			
		||||
import Link from "./Link";
 | 
			
		||||
 | 
			
		||||
loader.config({
 | 
			
		||||
  paths: {
 | 
			
		||||
    vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const DeployEditor = () => {
 | 
			
		||||
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const { theme } = useTheme();
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      css={{
 | 
			
		||||
        flex: 1,
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        position: "relative",
 | 
			
		||||
        flexDirection: "column",
 | 
			
		||||
        backgroundColor: "$mauve2",
 | 
			
		||||
        width: "100%",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <EditorNavigation showWat />
 | 
			
		||||
      {snap.files?.filter((file) => file.compiledWatContent).length > 0 &&
 | 
			
		||||
      router.isReady ? (
 | 
			
		||||
        <Editor
 | 
			
		||||
          className="hooks-editor"
 | 
			
		||||
          // keepCurrentModel
 | 
			
		||||
          defaultLanguage={snap.files?.[snap.active]?.language}
 | 
			
		||||
          language={snap.files?.[snap.active]?.language}
 | 
			
		||||
          path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
 | 
			
		||||
          value={snap.files?.[snap.active]?.compiledWatContent || ""}
 | 
			
		||||
          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;
 | 
			
		||||
            editor.updateOptions({
 | 
			
		||||
              glyphMargin: true,
 | 
			
		||||
              readOnly: true,
 | 
			
		||||
            });
 | 
			
		||||
          }}
 | 
			
		||||
          theme={theme === "dark" ? "dark" : "light"}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Container
 | 
			
		||||
          css={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            flex: 1,
 | 
			
		||||
            justifyContent: "center",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {!snap.loading && router.isReady && (
 | 
			
		||||
            <Text
 | 
			
		||||
              css={{
 | 
			
		||||
                mt: "-60px",
 | 
			
		||||
                fontSize: "14px",
 | 
			
		||||
                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>
 | 
			
		||||
          )}
 | 
			
		||||
        </Container>
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DeployEditor;
 | 
			
		||||
							
								
								
									
										103
									
								
								components/DeployFooter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,103 @@
 | 
			
		||||
import React, { useRef, useLayoutEffect } from "react";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import { Play, Prohibit } from "phosphor-react";
 | 
			
		||||
import useStayScrolled from "react-stay-scrolled";
 | 
			
		||||
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import LogText from "./LogText";
 | 
			
		||||
import { compileCode } from "../state/actions";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import Heading from "./Heading";
 | 
			
		||||
 | 
			
		||||
const Footer = () => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const logRef = useRef<HTMLPreElement>(null);
 | 
			
		||||
  const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
 | 
			
		||||
 | 
			
		||||
  useLayoutEffect(() => {
 | 
			
		||||
    stayScrolled();
 | 
			
		||||
  }, [snap.logs, stayScrolled]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      as="footer"
 | 
			
		||||
      css={{
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        borderTop: "1px solid $mauve6",
 | 
			
		||||
        background: "$mauve1",
 | 
			
		||||
        position: "relative",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Container css={{ py: "$3", flexShrink: 1 }}>
 | 
			
		||||
        <Heading
 | 
			
		||||
          as="h3"
 | 
			
		||||
          css={{ fontWeight: 300, m: 0, fontSize: "11px", color: "$mauve9" }}
 | 
			
		||||
        >
 | 
			
		||||
          DEVELOPMENT LOG
 | 
			
		||||
        </Heading>
 | 
			
		||||
        <Button
 | 
			
		||||
          ghost
 | 
			
		||||
          size="xs"
 | 
			
		||||
          css={{
 | 
			
		||||
            position: "absolute",
 | 
			
		||||
            right: "$3",
 | 
			
		||||
            top: "$2",
 | 
			
		||||
            color: "$mauve10",
 | 
			
		||||
          }}
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            state.logs = [];
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Prohibit size="14px" />
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Box
 | 
			
		||||
          as="pre"
 | 
			
		||||
          ref={logRef}
 | 
			
		||||
          css={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            flexDirection: "column",
 | 
			
		||||
            width: "100%",
 | 
			
		||||
            height: "160px",
 | 
			
		||||
            fontSize: "13px",
 | 
			
		||||
            fontWeight: "$body",
 | 
			
		||||
            fontFamily: "$monospace",
 | 
			
		||||
            overflowY: "auto",
 | 
			
		||||
            wordWrap: "break-word",
 | 
			
		||||
            py: 3,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {snap.logs?.map((log, index) => (
 | 
			
		||||
            <Box as="span" key={log.type + index}>
 | 
			
		||||
              <LogText capitalize variant={log.type}>
 | 
			
		||||
                {log.type}:{" "}
 | 
			
		||||
              </LogText>
 | 
			
		||||
              <LogText>{log.message}</LogText>
 | 
			
		||||
            </Box>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Box>
 | 
			
		||||
        <Button
 | 
			
		||||
          variant="primary"
 | 
			
		||||
          uppercase
 | 
			
		||||
          disabled={!snap.files.length}
 | 
			
		||||
          isLoading={snap.compiling}
 | 
			
		||||
          onClick={() => compileCode(snap.active)}
 | 
			
		||||
          css={{
 | 
			
		||||
            position: "absolute",
 | 
			
		||||
            bottom: "$4",
 | 
			
		||||
            left: "$4",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            cursor: "pointer",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Play weight="bold" size="16px" />
 | 
			
		||||
          Compile to Wasm
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Footer;
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
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 { blackA } from "@radix-ui/colors";
 | 
			
		||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
@@ -23,14 +23,14 @@ const StyledOverlay = styled(DialogPrimitive.Overlay, {
 | 
			
		||||
    animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
 | 
			
		||||
  },
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    backgroundColor: blackA.blackA9,
 | 
			
		||||
    backgroundColor: blackA.blackA11,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledContent = styled(DialogPrimitive.Content, {
 | 
			
		||||
  zIndex: 1000,
 | 
			
		||||
  backgroundColor: "$slate2",
 | 
			
		||||
  color: "$slate12",
 | 
			
		||||
  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)",
 | 
			
		||||
@@ -47,9 +47,9 @@ const StyledContent = styled(DialogPrimitive.Content, {
 | 
			
		||||
  },
 | 
			
		||||
  "&:focus": { outline: "none" },
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    backgroundColor: "$slate5",
 | 
			
		||||
    backgroundColor: "$mauve5",
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      "0px 10px 38px 0px rgba(22, 23, 24, 0.85), 0px 10px 20px 0px rgba(22, 23, 24, 0.6)",
 | 
			
		||||
      "0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -65,39 +65,21 @@ const Content: React.FC<{ css?: Stiches.CSS }> = ({ css, children }) => {
 | 
			
		||||
const StyledTitle = styled(DialogPrimitive.Title, {
 | 
			
		||||
  margin: 0,
 | 
			
		||||
  fontWeight: 500,
 | 
			
		||||
  color: "$slate12",
 | 
			
		||||
  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,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
export const Dialog = DialogPrimitive.Root;
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
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}` },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ import { keyframes } from "@stitches/react";
 | 
			
		||||
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)" },
 | 
			
		||||
@@ -26,7 +25,7 @@ const slideLeftAndFade = keyframes({
 | 
			
		||||
 | 
			
		||||
const StyledContent = styled(DropdownMenuPrimitive.Content, {
 | 
			
		||||
  minWidth: 220,
 | 
			
		||||
  backgroundColor: "$slate2",
 | 
			
		||||
  backgroundColor: "$mauve2",
 | 
			
		||||
  borderRadius: 6,
 | 
			
		||||
  padding: 5,
 | 
			
		||||
  boxShadow:
 | 
			
		||||
@@ -43,7 +42,7 @@ const StyledContent = styled(DropdownMenuPrimitive.Content, {
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    backgroundColor: "$slate5",
 | 
			
		||||
    backgroundColor: "$mauve5",
 | 
			
		||||
    boxShadow:
 | 
			
		||||
      "0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
 | 
			
		||||
  },
 | 
			
		||||
@@ -53,7 +52,7 @@ const itemStyles = {
 | 
			
		||||
  all: "unset",
 | 
			
		||||
  fontSize: 13,
 | 
			
		||||
  lineHeight: 1,
 | 
			
		||||
  color: "$slate12",
 | 
			
		||||
  color: "$mauve12",
 | 
			
		||||
  borderRadius: 3,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
@@ -62,15 +61,17 @@ const itemStyles = {
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  paddingLeft: "5px",
 | 
			
		||||
  userSelect: "none",
 | 
			
		||||
  py: "$0.5",
 | 
			
		||||
  pr: "$2",
 | 
			
		||||
  gap: "$2",
 | 
			
		||||
 | 
			
		||||
  "&[data-disabled]": {
 | 
			
		||||
    color: "$slate9",
 | 
			
		||||
    color: "$mauve9",
 | 
			
		||||
    pointerEvents: "none",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "&:focus": {
 | 
			
		||||
    backgroundColor: "$pink9",
 | 
			
		||||
    backgroundColor: "$purple9",
 | 
			
		||||
    color: "$white",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -84,8 +85,8 @@ const StyledRadioItem = styled(DropdownMenuPrimitive.RadioItem, {
 | 
			
		||||
});
 | 
			
		||||
const StyledTriggerItem = styled(DropdownMenuPrimitive.TriggerItem, {
 | 
			
		||||
  '&[data-state="open"]': {
 | 
			
		||||
    backgroundColor: "$pink9",
 | 
			
		||||
    color: "$pink9",
 | 
			
		||||
    backgroundColor: "$purple9",
 | 
			
		||||
    color: "$purple9",
 | 
			
		||||
  },
 | 
			
		||||
  ...itemStyles,
 | 
			
		||||
});
 | 
			
		||||
@@ -94,12 +95,12 @@ const StyledLabel = styled(DropdownMenuPrimitive.Label, {
 | 
			
		||||
  paddingLeft: 25,
 | 
			
		||||
  fontSize: 12,
 | 
			
		||||
  lineHeight: "25px",
 | 
			
		||||
  color: "$slate11",
 | 
			
		||||
  color: "$mauve11",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledSeparator = styled(DropdownMenuPrimitive.Separator, {
 | 
			
		||||
  height: 1,
 | 
			
		||||
  backgroundColor: "$slate7",
 | 
			
		||||
  backgroundColor: "$mauve7",
 | 
			
		||||
  margin: 5,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -113,7 +114,10 @@ const StyledItemIndicator = styled(DropdownMenuPrimitive.ItemIndicator, {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledArrow = styled(DropdownMenuPrimitive.Arrow, {
 | 
			
		||||
  fill: "$slate2",
 | 
			
		||||
  fill: "$mauve2",
 | 
			
		||||
  ".dark &": {
 | 
			
		||||
    fill: "$mauve5",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Exports
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,38 @@
 | 
			
		||||
import React, { useRef, useState } from "react";
 | 
			
		||||
import { Plus, Share, DownloadSimple, Gear, X } from "phosphor-react";
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import React, { useState, useEffect, useCallback } from "react";
 | 
			
		||||
import {
 | 
			
		||||
  Plus,
 | 
			
		||||
  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 { createNewFile, state, updateEditorSettings } from "../state";
 | 
			
		||||
import {
 | 
			
		||||
  createNewFile,
 | 
			
		||||
  syncToGist,
 | 
			
		||||
  updateEditorSettings,
 | 
			
		||||
  downloadAsZip,
 | 
			
		||||
} from "../state/actions";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
@@ -16,217 +46,498 @@ import {
 | 
			
		||||
} 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";
 | 
			
		||||
import Input from "./Input";
 | 
			
		||||
import Text from "./Text";
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import {
 | 
			
		||||
  AlertDialog,
 | 
			
		||||
  AlertDialogContent,
 | 
			
		||||
  AlertDialogTitle,
 | 
			
		||||
  AlertDialogDescription,
 | 
			
		||||
  AlertDialogCancel,
 | 
			
		||||
  AlertDialogAction,
 | 
			
		||||
} from "./AlertDialog";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const EditorNavigation = () => {
 | 
			
		||||
const DEFAULT_EXTENSION = ".c";
 | 
			
		||||
 | 
			
		||||
const ErrorText = styled(Text, {
 | 
			
		||||
  color: "$crimson9",
 | 
			
		||||
  mt: "$1",
 | 
			
		||||
  display: "block",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const [createNewAlertOpen, setCreateNewAlertOpen] = useState(false);
 | 
			
		||||
  const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
 | 
			
		||||
  const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
 | 
			
		||||
  const [newfileError, setNewfileError] = useState<string | null>(null);
 | 
			
		||||
  const [filename, setFilename] = useState("");
 | 
			
		||||
  const { theme } = useTheme();
 | 
			
		||||
  const { data: session, status } = useSession();
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const [popup, setPopUp] = useState(false);
 | 
			
		||||
  const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (session && session.user && popup) {
 | 
			
		||||
      setPopUp(false);
 | 
			
		||||
    }
 | 
			
		||||
  }, [session, popup]);
 | 
			
		||||
 | 
			
		||||
  // when filename changes, reset error
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setNewfileError(null);
 | 
			
		||||
  }, [filename, setNewfileError]);
 | 
			
		||||
 | 
			
		||||
  const validateFilename = useCallback(
 | 
			
		||||
    (filename: string): { error: string | null } => {
 | 
			
		||||
      // check if filename already exists
 | 
			
		||||
      if (snap.files.find(file => file.name === filename)) {
 | 
			
		||||
        return { error: "Filename already exists." };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // check for illegal characters
 | 
			
		||||
      const ILLEGAL_REGEX = /[/]/gi;
 | 
			
		||||
      if (filename.match(ILLEGAL_REGEX)) {
 | 
			
		||||
        return { error: "Filename contains illegal characters" };
 | 
			
		||||
      }
 | 
			
		||||
      // More checks in future
 | 
			
		||||
      return { error: null };
 | 
			
		||||
    },
 | 
			
		||||
    [snap.files]
 | 
			
		||||
  );
 | 
			
		||||
  const handleConfirm = useCallback(() => {
 | 
			
		||||
    // add default extension in case omitted
 | 
			
		||||
    let _filename = filename.includes(".")
 | 
			
		||||
      ? filename
 | 
			
		||||
      : filename + DEFAULT_EXTENSION;
 | 
			
		||||
    const chk = validateFilename(_filename);
 | 
			
		||||
    if (chk.error) {
 | 
			
		||||
      setNewfileError(`Error: ${chk.error}`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setIsNewfileDialogOpen(false);
 | 
			
		||||
    createNewFile(_filename);
 | 
			
		||||
    setFilename("");
 | 
			
		||||
  }, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
 | 
			
		||||
 | 
			
		||||
  const files = snap.files;
 | 
			
		||||
  return (
 | 
			
		||||
    <Flex css={{ flexShrink: 0, gap: "$0" }}>
 | 
			
		||||
      <Flex css={{ overflowX: "scroll", py: "$3", flex: 1 }}>
 | 
			
		||||
      <Flex
 | 
			
		||||
        css={{
 | 
			
		||||
          overflowX: "scroll",
 | 
			
		||||
          py: "$3",
 | 
			
		||||
          flex: 1,
 | 
			
		||||
          "&::-webkit-scrollbar": {
 | 
			
		||||
            height: 0,
 | 
			
		||||
            background: "transparent",
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <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"
 | 
			
		||||
          <Stack
 | 
			
		||||
            css={{
 | 
			
		||||
              gap: "$3",
 | 
			
		||||
              flex: 1,
 | 
			
		||||
              flexWrap: "nowrap",
 | 
			
		||||
              marginBottom: "-1px",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {files &&
 | 
			
		||||
              files.length > 0 &&
 | 
			
		||||
              files.map((file, index) => {
 | 
			
		||||
                if (!file.compiledContent && showWat) {
 | 
			
		||||
                  return null;
 | 
			
		||||
                }
 | 
			
		||||
                return (
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    outline={
 | 
			
		||||
                      showWat ? snap.activeWat !== index : snap.active !== index
 | 
			
		||||
                    }
 | 
			
		||||
                    onClick={() => (state.active = index)}
 | 
			
		||||
                    key={file.name + index}
 | 
			
		||||
                    css={{
 | 
			
		||||
                      display: "flex",
 | 
			
		||||
                      p: "1px",
 | 
			
		||||
                      borderRadius: "$full",
 | 
			
		||||
                      mr: "-4px",
 | 
			
		||||
                      "&:hover": {
 | 
			
		||||
                        span: {
 | 
			
		||||
                          visibility: "visible",
 | 
			
		||||
                        },
 | 
			
		||||
                      },
 | 
			
		||||
                    }}
 | 
			
		||||
                    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("");
 | 
			
		||||
                    {file.name}
 | 
			
		||||
                    {showWat && ".wat"}
 | 
			
		||||
                    {!showWat && (
 | 
			
		||||
                      <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();
 | 
			
		||||
                          // Remove file from state
 | 
			
		||||
                          state.files.splice(index, 1);
 | 
			
		||||
                          // Change active file state
 | 
			
		||||
                          // If deleted file is behind active tab
 | 
			
		||||
                          // we keep the current state otherwise
 | 
			
		||||
                          // select previous file on the list
 | 
			
		||||
                          state.active =
 | 
			
		||||
                            index > snap.active ? snap.active : snap.active - 1;
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <X size="9px" weight="bold" />
 | 
			
		||||
                      </Box>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                );
 | 
			
		||||
              })}
 | 
			
		||||
            {!showWat && (
 | 
			
		||||
              <Dialog
 | 
			
		||||
                open={isNewfileDialogOpen}
 | 
			
		||||
                onOpenChange={setIsNewfileDialogOpen}
 | 
			
		||||
              >
 | 
			
		||||
                <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>
 | 
			
		||||
                    <label>Filename</label>
 | 
			
		||||
                    <Input
 | 
			
		||||
                      value={filename}
 | 
			
		||||
                      onChange={(e) => setFilename(e.target.value)}
 | 
			
		||||
                      onKeyPress={(e) => {
 | 
			
		||||
                        if (e.key === "Enter") {
 | 
			
		||||
                          handleConfirm();
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                    />
 | 
			
		||||
                    <ErrorText>{newfileError}</ErrorText>
 | 
			
		||||
                  </DialogDescription>
 | 
			
		||||
 | 
			
		||||
                  <Flex
 | 
			
		||||
                    css={{
 | 
			
		||||
                      marginTop: 25,
 | 
			
		||||
                      justifyContent: "flex-end",
 | 
			
		||||
                      gap: "$3",
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                      <Button outline>Cancel</Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                    <Button variant="primary" onClick={handleConfirm}>
 | 
			
		||||
                      Create file
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </Flex>
 | 
			
		||||
                  <DialogClose asChild>
 | 
			
		||||
                    <Box css={{ position: "absolute", top: "$3", right: "$3" }}>
 | 
			
		||||
                      <X size="20px" />
 | 
			
		||||
                    </Box>
 | 
			
		||||
                  </DialogClose>
 | 
			
		||||
                </Flex>
 | 
			
		||||
                <DialogClose asChild>
 | 
			
		||||
                  <Box css={{ position: "absolute", top: "$3", right: "$3" }}>
 | 
			
		||||
                    <X size="20px" />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                </DialogClose>
 | 
			
		||||
              </DialogContent>
 | 
			
		||||
            </Dialog>
 | 
			
		||||
                </DialogContent>
 | 
			
		||||
              </Dialog>
 | 
			
		||||
            )}
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </Container>
 | 
			
		||||
      </Flex>
 | 
			
		||||
      <Flex
 | 
			
		||||
        css={{
 | 
			
		||||
          py: "$3",
 | 
			
		||||
          backgroundColor: "$slate3",
 | 
			
		||||
          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",
 | 
			
		||||
              flexShrink: 0,
 | 
			
		||||
              gap: "$0",
 | 
			
		||||
              border: "1px solid $slate10",
 | 
			
		||||
              borderRadius: "$sm",
 | 
			
		||||
              boxShadow: "inset 0px 0px 0px 1px $colors$mauve10",
 | 
			
		||||
              zIndex: 9,
 | 
			
		||||
              position: "relative",
 | 
			
		||||
              overflow: "hidden",
 | 
			
		||||
              button: {
 | 
			
		||||
                borderRadius: "$0",
 | 
			
		||||
                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" }}>
 | 
			
		||||
            <Button
 | 
			
		||||
              isLoading={snap.zipLoading}
 | 
			
		||||
              onClick={downloadAsZip}
 | 
			
		||||
              outline
 | 
			
		||||
              size="sm"
 | 
			
		||||
              css={{ alignItems: "center" }}
 | 
			
		||||
            >
 | 
			
		||||
              <DownloadSimple size="16px" />
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Dialog>
 | 
			
		||||
              <DialogTrigger asChild>
 | 
			
		||||
                <Button ghost size="sm" css={{ alignItems: "center" }}>
 | 
			
		||||
            <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>
 | 
			
		||||
            <Button
 | 
			
		||||
              outline
 | 
			
		||||
              size="sm"
 | 
			
		||||
              disabled={!session || !session.user}
 | 
			
		||||
              isLoading={snap.gistLoading}
 | 
			
		||||
              css={{ alignItems: "center" }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                if (snap.gistOwner === session?.user.username) {
 | 
			
		||||
                  syncToGist(session);
 | 
			
		||||
                } else {
 | 
			
		||||
                  setCreateNewAlertOpen(true);
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <CloudArrowUp size="16px" />
 | 
			
		||||
            </Button>
 | 
			
		||||
 | 
			
		||||
            <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={() => {
 | 
			
		||||
                    setCreateNewAlertOpen(true);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <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>
 | 
			
		||||
      <AlertDialog
 | 
			
		||||
        open={createNewAlertOpen}
 | 
			
		||||
        onOpenChange={(value) => setCreateNewAlertOpen(value)}
 | 
			
		||||
      >
 | 
			
		||||
        <AlertDialogContent>
 | 
			
		||||
          <AlertDialogTitle>Are you sure?</AlertDialogTitle>
 | 
			
		||||
          <AlertDialogDescription>
 | 
			
		||||
            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.
 | 
			
		||||
          </AlertDialogDescription>
 | 
			
		||||
          <Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
 | 
			
		||||
            <AlertDialogCancel asChild>
 | 
			
		||||
              <Button outline>Cancel</Button>
 | 
			
		||||
            </AlertDialogCancel>
 | 
			
		||||
            <AlertDialogAction asChild>
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="primary"
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  syncToGist(session, true);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <FilePlus size="15px" /> Create new Gist
 | 
			
		||||
              </Button>
 | 
			
		||||
            </AlertDialogAction>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </AlertDialogContent>
 | 
			
		||||
      </AlertDialog>
 | 
			
		||||
 | 
			
		||||
      <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>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,23 @@ import Box from "./Box";
 | 
			
		||||
 | 
			
		||||
const Flex = styled(Box, {
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  variants: {
 | 
			
		||||
    row: {
 | 
			
		||||
      true: {
 | 
			
		||||
        flexDirection: "row",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    column: {
 | 
			
		||||
      true: {
 | 
			
		||||
        flexDirection: "column",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    fluid: {
 | 
			
		||||
      true: {
 | 
			
		||||
        width: '100%'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
@@ -4,7 +4,13 @@ const Heading = styled("span", {
 | 
			
		||||
  fontFamily: "$heading",
 | 
			
		||||
  lineHeight: "$heading",
 | 
			
		||||
  fontWeight: "$heading",
 | 
			
		||||
  textTransform: "uppercase",
 | 
			
		||||
  variants: {
 | 
			
		||||
    uppercase: {
 | 
			
		||||
      true: {
 | 
			
		||||
        textTransform: "uppercase",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Heading;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,57 @@
 | 
			
		||||
import React, { useEffect, useRef } from "react";
 | 
			
		||||
import { useSnapshot, ref } from "valtio";
 | 
			
		||||
import Editor from "@monaco-editor/react";
 | 
			
		||||
import Editor, { loader } from "@monaco-editor/react";
 | 
			
		||||
import type monaco from "monaco-editor";
 | 
			
		||||
import { Play } from "phosphor-react";
 | 
			
		||||
import { ArrowBendLeftUp } from "phosphor-react";
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import dark from "../theme/editor/amy.json";
 | 
			
		||||
import light from "../theme/editor/xcode_default.json";
 | 
			
		||||
import { compileCode, saveFile, state } from "../state";
 | 
			
		||||
import { saveFile } from "../state/actions";
 | 
			
		||||
import { apiHeaderFiles } from "../state/constants";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
 | 
			
		||||
import EditorNavigation from "./EditorNavigation";
 | 
			
		||||
import Spinner from "./Spinner";
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
loader.config({
 | 
			
		||||
  paths: {
 | 
			
		||||
    vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
 | 
			
		||||
  const currPath = editor.getModel()?.uri.path;
 | 
			
		||||
  if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
 | 
			
		||||
    editor.updateOptions({ readOnly: true });
 | 
			
		||||
  } else {
 | 
			
		||||
    editor.updateOptions({ readOnly: false });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const HooksEditor = () => {
 | 
			
		||||
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
 | 
			
		||||
  const subscriptionRef = useRef<ReconnectingWebSocket | null>(null);
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  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");
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (editorRef.current) validateWritability(editorRef.current);
 | 
			
		||||
  }, [snap.active]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    return () => {
 | 
			
		||||
      subscriptionRef?.current?.close();
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      css={{
 | 
			
		||||
@@ -36,61 +60,126 @@ const HooksEditor = () => {
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        position: "relative",
 | 
			
		||||
        flexDirection: "column",
 | 
			
		||||
        backgroundColor: "$slate3",
 | 
			
		||||
        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());
 | 
			
		||||
      {snap.files.length > 0 && router.isReady ? (
 | 
			
		||||
        <Editor
 | 
			
		||||
          className="hooks-editor"
 | 
			
		||||
          keepCurrentModel
 | 
			
		||||
          defaultLanguage={snap.files?.[snap.active]?.language}
 | 
			
		||||
          language={snap.files?.[snap.active]?.language}
 | 
			
		||||
          path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
 | 
			
		||||
          defaultValue={snap.files?.[snap.active]?.content}
 | 
			
		||||
          beforeMount={(monaco) => {
 | 
			
		||||
            if (!snap.editorCtx) {
 | 
			
		||||
              snap.files.forEach((file) =>
 | 
			
		||||
                monaco.editor.createModel(
 | 
			
		||||
                  file.content,
 | 
			
		||||
                  file.language,
 | 
			
		||||
                  monaco.Uri.parse(`file:///work/c/${file.name}`)
 | 
			
		||||
                )
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          );
 | 
			
		||||
        }}
 | 
			
		||||
        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>
 | 
			
		||||
 | 
			
		||||
            // create the web socket
 | 
			
		||||
            if (!subscriptionRef.current) {
 | 
			
		||||
              monaco.languages.register({
 | 
			
		||||
                id: "c",
 | 
			
		||||
                extensions: [".c", ".h"],
 | 
			
		||||
                aliases: ["C", "c", "H", "h"],
 | 
			
		||||
                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.stop();
 | 
			
		||||
                      disposable.dispose();
 | 
			
		||||
                    } catch (err) {
 | 
			
		||||
                      console.log("err", err);
 | 
			
		||||
                    }
 | 
			
		||||
                  });
 | 
			
		||||
                },
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // // hook editor to global state
 | 
			
		||||
            // editor.updateOptions({
 | 
			
		||||
            //   minimap: {
 | 
			
		||||
            //     enabled: false,
 | 
			
		||||
            //   },
 | 
			
		||||
            //   ...snap.editorSettings,
 | 
			
		||||
            // });
 | 
			
		||||
            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;
 | 
			
		||||
            editor.updateOptions({
 | 
			
		||||
              glyphMargin: true,
 | 
			
		||||
              lightbulb: {
 | 
			
		||||
                enabled: true,
 | 
			
		||||
              },
 | 
			
		||||
            });
 | 
			
		||||
            editor.addCommand(
 | 
			
		||||
              monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
 | 
			
		||||
              () => {
 | 
			
		||||
                saveFile();
 | 
			
		||||
              }
 | 
			
		||||
            );
 | 
			
		||||
            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>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										151
									
								
								components/Input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,151 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
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",
 | 
			
		||||
    "&: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",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Input;
 | 
			
		||||
							
								
								
									
										8
									
								
								components/Link.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const StyledLink = styled("a", {
 | 
			
		||||
  color: "CurrentColor",
 | 
			
		||||
  textDecoration: "underline",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default StyledLink;
 | 
			
		||||
							
								
								
									
										140
									
								
								components/LogBox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,140 @@
 | 
			
		||||
import React, { useRef, useLayoutEffect, ReactNode } from "react";
 | 
			
		||||
import { Notepad, Prohibit } from "phosphor-react";
 | 
			
		||||
import useStayScrolled from "react-stay-scrolled";
 | 
			
		||||
import NextLink from "next/link";
 | 
			
		||||
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Flex from "./Flex";
 | 
			
		||||
import LogText from "./LogText";
 | 
			
		||||
import { ILog } from "../state";
 | 
			
		||||
import Text from "./Text";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import Heading from "./Heading";
 | 
			
		||||
import Link from "./Link";
 | 
			
		||||
 | 
			
		||||
interface ILogBox {
 | 
			
		||||
  title: string;
 | 
			
		||||
  clearLog?: () => void;
 | 
			
		||||
  logs: ILog[];
 | 
			
		||||
  renderNav?: () => ReactNode;
 | 
			
		||||
  enhanced?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const LogBox: React.FC<ILogBox> = ({
 | 
			
		||||
  title,
 | 
			
		||||
  clearLog,
 | 
			
		||||
  logs,
 | 
			
		||||
  children,
 | 
			
		||||
  renderNav,
 | 
			
		||||
  enhanced,
 | 
			
		||||
}) => {
 | 
			
		||||
  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
 | 
			
		||||
          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",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
 | 
			
		||||
          </Heading>
 | 
			
		||||
          {renderNav?.()}
 | 
			
		||||
          <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 ? "$2 $1" : undefined,
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <LogText variant={log.type}>
 | 
			
		||||
                {log.message}{" "}
 | 
			
		||||
                {log.link && (
 | 
			
		||||
                  <NextLink href={log.link} shallow passHref>
 | 
			
		||||
                    <Link as="a">{log.linkText}</Link>
 | 
			
		||||
                  </NextLink>
 | 
			
		||||
                )}
 | 
			
		||||
              </LogText>
 | 
			
		||||
            </Box>
 | 
			
		||||
          ))}
 | 
			
		||||
          {children}
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default LogBox;
 | 
			
		||||
@@ -4,16 +4,20 @@ const Text = styled("span", {
 | 
			
		||||
  fontFamily: "$monospace",
 | 
			
		||||
  lineHeight: "$body",
 | 
			
		||||
  color: "$text",
 | 
			
		||||
  wordWrap: "break-word",
 | 
			
		||||
  variants: {
 | 
			
		||||
    variant: {
 | 
			
		||||
      log: {
 | 
			
		||||
        color: "$text",
 | 
			
		||||
      },
 | 
			
		||||
      warning: {
 | 
			
		||||
        color: "$yellow11",
 | 
			
		||||
        color: "$amber11",
 | 
			
		||||
      },
 | 
			
		||||
      error: {
 | 
			
		||||
        color: "$red11",
 | 
			
		||||
        color: "$crimson11",
 | 
			
		||||
      },
 | 
			
		||||
      success: {
 | 
			
		||||
        color: "$grass11",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    capitalize: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,8 @@
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const SVG = styled("svg", {
 | 
			
		||||
  "& #path-one, & #path-two": {
 | 
			
		||||
    fill: "$text",
 | 
			
		||||
  "& #path": {
 | 
			
		||||
    fill: "$accent",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
function Logo({
 | 
			
		||||
@@ -16,21 +14,18 @@ function Logo({
 | 
			
		||||
}) {
 | 
			
		||||
  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>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,157 +1,413 @@
 | 
			
		||||
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 { 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 Flex from "./Flex";
 | 
			
		||||
import ThemeChanger from "./ThemeChanger";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
const Navigation = () => {
 | 
			
		||||
  const { data: session, status } = useSession();
 | 
			
		||||
  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",
 | 
			
		||||
        backgroundColor: "$mauve1",
 | 
			
		||||
        borderBottom: "1px solid $mauve6",
 | 
			
		||||
        position: "relative",
 | 
			
		||||
        zIndex: 2003,
 | 
			
		||||
        height: "60px",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Container
 | 
			
		||||
        css={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          py: "$2",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <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.files?.[0]?.name || "XRPL Hooks"}
 | 
			
		||||
                </Heading>
 | 
			
		||||
                <Text
 | 
			
		||||
                  css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
 | 
			
		||||
                >
 | 
			
		||||
                  {snap.files.length > 0 ? "Gist: " : "Playground"}
 | 
			
		||||
                  {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={{
 | 
			
		||||
                    maxWidth: "100%",
 | 
			
		||||
                    width: "80vw",
 | 
			
		||||
                    height: "80%",
 | 
			
		||||
                    backgroundColor: "$mauve1 !important",
 | 
			
		||||
                    overflowY: "auto",
 | 
			
		||||
                    p: 0,
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Flex
 | 
			
		||||
                    css={{
 | 
			
		||||
                      flexDirection: "column",
 | 
			
		||||
                      flex: 1,
 | 
			
		||||
                      height: "auto",
 | 
			
		||||
                      "@md": {
 | 
			
		||||
                        flexDirection: "row",
 | 
			
		||||
                        height: "100%",
 | 
			
		||||
                      },
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Flex
 | 
			
		||||
                      css={{
 | 
			
		||||
                        borderBottom: "1px solid $colors$mauve5",
 | 
			
		||||
                        width: "100%",
 | 
			
		||||
                        flexDirection: "column",
 | 
			
		||||
                        p: "$7",
 | 
			
		||||
                        height: "100%",
 | 
			
		||||
                        backgroundColor: "$mauve2",
 | 
			
		||||
                        "@md": {
 | 
			
		||||
                          width: "30%",
 | 
			
		||||
                          maxWidth: "300px",
 | 
			
		||||
                          borderBottom: "0px",
 | 
			
		||||
                          borderRight: "1px solid $colors$mauve6",
 | 
			
		||||
                        },
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <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: "$purple10",
 | 
			
		||||
                              "&:hover": {
 | 
			
		||||
                                color: "$purple11",
 | 
			
		||||
                              },
 | 
			
		||||
                              "&: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: "$purple10",
 | 
			
		||||
                              "&:hover": {
 | 
			
		||||
                                color: "$purple11",
 | 
			
		||||
                              },
 | 
			
		||||
                              "&:focus": {
 | 
			
		||||
                                outline: 0,
 | 
			
		||||
                              },
 | 
			
		||||
                            }}
 | 
			
		||||
                            as="a"
 | 
			
		||||
                            rel="noreferrer noopener"
 | 
			
		||||
                            target="_blank"
 | 
			
		||||
                            href="https://xrpl-hooks.readme.io/docs"
 | 
			
		||||
                          >
 | 
			
		||||
                            <ArrowUpRight size="15px" /> Hooks documentation
 | 
			
		||||
                          </Text>
 | 
			
		||||
                          <Text
 | 
			
		||||
                            css={{
 | 
			
		||||
                              display: "inline-flex",
 | 
			
		||||
                              alignItems: "center",
 | 
			
		||||
                              gap: "$3",
 | 
			
		||||
                              color: "$purple10",
 | 
			
		||||
                              "&:hover": {
 | 
			
		||||
                                color: "$purple11",
 | 
			
		||||
                              },
 | 
			
		||||
                              "&: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",
 | 
			
		||||
                        gap: "$3",
 | 
			
		||||
                        alignItems: "flex-start",
 | 
			
		||||
                        flexWrap: "wrap",
 | 
			
		||||
                        backgroundColor: "$mauve1",
 | 
			
		||||
                        "@md": {
 | 
			
		||||
                          gridTemplateColumns: "1fr 1fr 1fr",
 | 
			
		||||
                          gridTemplateRows: "max-content",
 | 
			
		||||
                        },
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.starter}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Starter</Heading>
 | 
			
		||||
                        <Text>
 | 
			
		||||
                          Just a basic starter with essential imports
 | 
			
		||||
                        </Text>
 | 
			
		||||
                      </PanelBox>
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.firewall}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Firewall</Heading>
 | 
			
		||||
                        <Text>
 | 
			
		||||
                          This Hook essentially checks a blacklist of accounts
 | 
			
		||||
                        </Text>
 | 
			
		||||
                      </PanelBox>
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.notary}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Notary</Heading>
 | 
			
		||||
                        <Text>
 | 
			
		||||
                          Collecting signatures for multi-sign transactions
 | 
			
		||||
                        </Text>
 | 
			
		||||
                      </PanelBox>
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.carbon}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Carbon</Heading>
 | 
			
		||||
                        <Text>Send a percentage of sum to an address</Text>
 | 
			
		||||
                      </PanelBox>
 | 
			
		||||
                      <PanelBox
 | 
			
		||||
                        as="a"
 | 
			
		||||
                        href={`/develop/${templateFileIds.peggy}`}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Heading>Peggy</Heading>
 | 
			
		||||
                        <Text>An oracle based stable coin hook</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",
 | 
			
		||||
            },
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <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/" passHref>
 | 
			
		||||
              <a target="_blank" rel="noreferrer noopener">
 | 
			
		||||
                <Button outline>
 | 
			
		||||
                  <BookOpen size="15px" />
 | 
			
		||||
                </Button>
 | 
			
		||||
              </a>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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;
 | 
			
		||||
							
								
								
									
										56
									
								
								components/Select.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,56 @@
 | 
			
		||||
import { FC } from "react";
 | 
			
		||||
import { mauve, mauveDark } 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 });
 | 
			
		||||
 | 
			
		||||
const Select: FC<Props> = props => {
 | 
			
		||||
  const { theme } = useTheme();
 | 
			
		||||
  const isDark = theme === "dark";
 | 
			
		||||
  const colors: any = {
 | 
			
		||||
    // primary: pink.pink9,
 | 
			
		||||
    primary: isDark ? mauveDark.mauve4 : mauve.mauve4,
 | 
			
		||||
    secondary: isDark ? mauveDark.mauve8 : mauve.mauve8,
 | 
			
		||||
    background: isDark ? "rgb(10, 10, 10)" : "rgb(245, 245, 245)",
 | 
			
		||||
    searchText: isDark ? mauveDark.mauve12 : mauve.mauve12,
 | 
			
		||||
    placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11,
 | 
			
		||||
  };
 | 
			
		||||
  colors.outline = colors.background;
 | 
			
		||||
  colors.selected = colors.secondary;
 | 
			
		||||
  return (
 | 
			
		||||
    <SelectInput
 | 
			
		||||
      menuPosition="fixed"
 | 
			
		||||
      theme={theme => ({
 | 
			
		||||
        ...theme,
 | 
			
		||||
        spacing: {
 | 
			
		||||
          ...theme.spacing,
 | 
			
		||||
          controlHeight: 30
 | 
			
		||||
        },
 | 
			
		||||
        colors: {
 | 
			
		||||
          primary: colors.selected,
 | 
			
		||||
          primary25: colors.primary,
 | 
			
		||||
          primary50: colors.primary,
 | 
			
		||||
          primary75: colors.primary,
 | 
			
		||||
          danger: colors.primary,
 | 
			
		||||
          dangerLight: colors.primary,
 | 
			
		||||
          neutral0: colors.background,
 | 
			
		||||
          neutral5: colors.primary,
 | 
			
		||||
          neutral10: colors.primary,
 | 
			
		||||
          neutral20: colors.outline,
 | 
			
		||||
          neutral30: colors.primary,
 | 
			
		||||
          neutral40: colors.primary,
 | 
			
		||||
          neutral50: colors.placeholder,
 | 
			
		||||
          neutral60: colors.primary,
 | 
			
		||||
          neutral70: colors.primary,
 | 
			
		||||
          neutral80: colors.searchText,
 | 
			
		||||
          neutral90: colors.primary,
 | 
			
		||||
        },
 | 
			
		||||
      })}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default styled(Select, {});
 | 
			
		||||
@@ -3,11 +3,12 @@ import { styled, keyframes } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const rotate = keyframes({
 | 
			
		||||
  "0%": { transform: "rotate(0deg)" },
 | 
			
		||||
  "100%": { transform: "rotate(360deg)" },
 | 
			
		||||
  "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;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,5 @@
 | 
			
		||||
import { Children } from "react";
 | 
			
		||||
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
import type * as Stitches from "@stitches/react";
 | 
			
		||||
 | 
			
		||||
const StackComponent = styled(Box, {
 | 
			
		||||
  display: "flex",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										257
									
								
								components/Tabs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,257 @@
 | 
			
		||||
import React, {
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useState,
 | 
			
		||||
  Fragment,
 | 
			
		||||
  isValidElement,
 | 
			
		||||
  useCallback,
 | 
			
		||||
} from "react";
 | 
			
		||||
import type { ReactNode, ReactElement } from "react";
 | 
			
		||||
import { Box, Button, Flex, Input, Stack, Text } from ".";
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogClose,
 | 
			
		||||
} from "./Dialog";
 | 
			
		||||
import { Plus, X } from "phosphor-react";
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const ErrorText = styled(Text, {
 | 
			
		||||
  color: "$crimson9",
 | 
			
		||||
  mt: "$1",
 | 
			
		||||
  display: "block",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
interface TabProps {
 | 
			
		||||
  header?: string;
 | 
			
		||||
  children: ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO customise strings shown
 | 
			
		||||
interface Props {
 | 
			
		||||
  activeIndex?: number;
 | 
			
		||||
  activeHeader?: string;
 | 
			
		||||
  headless?: boolean;
 | 
			
		||||
  children: ReactElement<TabProps>[];
 | 
			
		||||
  keepAllAlive?: boolean;
 | 
			
		||||
  defaultExtension?: string;
 | 
			
		||||
  forceDefaultExtension?: boolean;
 | 
			
		||||
  onCreateNewTab?: (name: string) => any;
 | 
			
		||||
  onCloseTab?: (index: number, header?: string) => any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Tab = (props: TabProps) => null;
 | 
			
		||||
 | 
			
		||||
export const Tabs = ({
 | 
			
		||||
  children,
 | 
			
		||||
  activeIndex,
 | 
			
		||||
  activeHeader,
 | 
			
		||||
  headless,
 | 
			
		||||
  keepAllAlive = false,
 | 
			
		||||
  onCreateNewTab,
 | 
			
		||||
  onCloseTab,
 | 
			
		||||
  defaultExtension = "",
 | 
			
		||||
  forceDefaultExtension,
 | 
			
		||||
}: Props) => {
 | 
			
		||||
  const [active, setActive] = useState(activeIndex || 0);
 | 
			
		||||
  const tabs: TabProps[] = children.map((elem) => elem.props);
 | 
			
		||||
 | 
			
		||||
  const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
 | 
			
		||||
  const [tabname, setTabname] = useState("");
 | 
			
		||||
  const [newtabError, setNewtabError] = useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (activeIndex) setActive(activeIndex);
 | 
			
		||||
  }, [activeIndex]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (activeHeader) {
 | 
			
		||||
      const idx = tabs.findIndex((tab) => tab.header === activeHeader);
 | 
			
		||||
      setActive(idx);
 | 
			
		||||
    }
 | 
			
		||||
  }, [activeHeader, tabs]);
 | 
			
		||||
 | 
			
		||||
  // when filename changes, reset error
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setNewtabError(null);
 | 
			
		||||
  }, [tabname, setNewtabError]);
 | 
			
		||||
 | 
			
		||||
  const validateTabname = useCallback(
 | 
			
		||||
    (tabname: string): { error: string | null } => {
 | 
			
		||||
      if (tabs.find((tab) => tab.header === tabname)) {
 | 
			
		||||
        return { error: "Name already exists." };
 | 
			
		||||
      }
 | 
			
		||||
      return { error: null };
 | 
			
		||||
    },
 | 
			
		||||
    [tabs]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleCreateTab = useCallback(() => {
 | 
			
		||||
    // add default extension in case omitted
 | 
			
		||||
    let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
 | 
			
		||||
    if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
 | 
			
		||||
      _tabname = _tabname + defaultExtension;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const chk = validateTabname(_tabname);
 | 
			
		||||
    if (chk.error) {
 | 
			
		||||
      setNewtabError(`Error: ${chk.error}`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setIsNewtabDialogOpen(false);
 | 
			
		||||
    setTabname("");
 | 
			
		||||
    // switch to new tab?
 | 
			
		||||
    setActive(tabs.length);
 | 
			
		||||
 | 
			
		||||
    onCreateNewTab?.(_tabname);
 | 
			
		||||
  }, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]);
 | 
			
		||||
 | 
			
		||||
  const handleCloseTab = useCallback(
 | 
			
		||||
    (idx: number) => {
 | 
			
		||||
      if (idx <= active && active !== 0) {
 | 
			
		||||
        setActive(active - 1);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      onCloseTab?.(idx, tabs[idx].header);
 | 
			
		||||
    },
 | 
			
		||||
    [active, onCloseTab, tabs]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {!headless && (
 | 
			
		||||
        <Stack
 | 
			
		||||
          css={{
 | 
			
		||||
            gap: "$3",
 | 
			
		||||
            flex: 1,
 | 
			
		||||
            flexWrap: "nowrap",
 | 
			
		||||
            marginBottom: "-1px",
 | 
			
		||||
            width: "100%",
 | 
			
		||||
            overflow: "auto",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {tabs.map((tab, idx) => (
 | 
			
		||||
            <Button
 | 
			
		||||
              key={tab.header}
 | 
			
		||||
              role="tab"
 | 
			
		||||
              tabIndex={idx}
 | 
			
		||||
              onClick={() => setActive(idx)}
 | 
			
		||||
              onKeyPress={() => setActive(idx)}
 | 
			
		||||
              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>
 | 
			
		||||
          ))}
 | 
			
		||||
          {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 tab"}
 | 
			
		||||
                </Button>
 | 
			
		||||
              </DialogTrigger>
 | 
			
		||||
              <DialogContent>
 | 
			
		||||
                <DialogTitle>Create new tab</DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                  <label>Tabname</label>
 | 
			
		||||
                  <Input
 | 
			
		||||
                    value={tabname}
 | 
			
		||||
                    onChange={(e) => setTabname(e.target.value)}
 | 
			
		||||
                    onKeyPress={(e) => {
 | 
			
		||||
                      if (e.key === "Enter") {
 | 
			
		||||
                        handleCreateTab();
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <ErrorText>{newtabError}</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>
 | 
			
		||||
          )}
 | 
			
		||||
        </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 }}
 | 
			
		||||
            />
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Fragment key={tabs[active].header || active}>
 | 
			
		||||
          {tabs[active].children}
 | 
			
		||||
        </Fragment>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -4,6 +4,18 @@ const Text = styled("span", {
 | 
			
		||||
  fontFamily: "$body",
 | 
			
		||||
  lineHeight: "$body",
 | 
			
		||||
  color: "$text",
 | 
			
		||||
  variants: {
 | 
			
		||||
    small: {
 | 
			
		||||
      true: {
 | 
			
		||||
        fontSize: '$xs'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    muted: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: '$mauve9'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Text;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ 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();
 | 
			
		||||
@@ -12,7 +12,8 @@ const ThemeChanger = () => {
 | 
			
		||||
  if (!mounted) return null;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
    <Button
 | 
			
		||||
      outline
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        setTheme(theme && theme === "light" ? "dark" : "light");
 | 
			
		||||
      }}
 | 
			
		||||
@@ -25,12 +26,8 @@ const ThemeChanger = () => {
 | 
			
		||||
        color: "$muted",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {theme === "dark" ? (
 | 
			
		||||
        <Sun weight="bold" size="16px" />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Moon weight="bold" size="16px" />
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
      {theme === "dark" ? <Sun size="15px" /> : <Moon size="15px" />}
 | 
			
		||||
    </Button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								components/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
export { default as Flex } from './Flex'
 | 
			
		||||
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 } from './Input'
 | 
			
		||||
export { default as Select } from './Select'
 | 
			
		||||
export * from './Tabs'
 | 
			
		||||
export * from './AlertDialog'
 | 
			
		||||
export { default as Box } from './Box'
 | 
			
		||||
export { default as Button } from './Button'
 | 
			
		||||
export { default as ButtonGroup } from './ButtonGroup'
 | 
			
		||||
export { default as DeployFooter } from './DeployFooter'
 | 
			
		||||
export * from './Dialog'
 | 
			
		||||
export * from './DropdownMenu'
 | 
			
		||||
							
								
								
									
										221
									
								
								content/transactions.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,221 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "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": "currency"
 | 
			
		||||
    },
 | 
			
		||||
    "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
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "EscrowCreate",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
    },
 | 
			
		||||
    "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
 | 
			
		||||
    "CancelAfter": 533257958,
 | 
			
		||||
    "FinishAfter": 533171558,
 | 
			
		||||
    "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
 | 
			
		||||
    "DestinationTag": 23480,
 | 
			
		||||
    "SourceTag": 11747
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "EscrowFinish",
 | 
			
		||||
    "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "OfferSequence": 7,
 | 
			
		||||
    "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
 | 
			
		||||
    "Fulfillment": "A0028000"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenBurn",
 | 
			
		||||
    "Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenAcceptOffer",
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenCancelOffer",
 | 
			
		||||
    "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "NFTokenCreateOffer",
 | 
			
		||||
    "Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
 | 
			
		||||
    "TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
    },
 | 
			
		||||
    "Flags": 1
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "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": "currency"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "Payment",
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
    },
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": 2147483648,
 | 
			
		||||
    "Sequence": 2
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "PaymentChannelCreate",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
    },
 | 
			
		||||
    "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
 | 
			
		||||
    "SettleDelay": 86400,
 | 
			
		||||
    "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
 | 
			
		||||
    "CancelAfter": 533171558,
 | 
			
		||||
    "DestinationTag": 23480,
 | 
			
		||||
    "SourceTag": 11747
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "TransactionType": "PaymentChannelFund",
 | 
			
		||||
    "Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "200",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
    },
 | 
			
		||||
    "Expiration": 543171558
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "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,
 | 
			
		||||
    "Amount": {
 | 
			
		||||
      "value": "100",
 | 
			
		||||
      "type": "currency"
 | 
			
		||||
    },
 | 
			
		||||
    "Sequence": 12
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
@@ -2,6 +2,15 @@
 | 
			
		||||
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;
 | 
			
		||||
    }
 | 
			
		||||
    return config;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -6,27 +6,55 @@
 | 
			
		||||
    "dev": "next dev",
 | 
			
		||||
    "build": "next build",
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint"
 | 
			
		||||
    "lint": "next lint",
 | 
			
		||||
    "postinstall": "patch-package"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@codingame/monaco-jsonrpc": "^0.3.1",
 | 
			
		||||
    "@codingame/monaco-languageclient": "^0.17.0",
 | 
			
		||||
    "@monaco-editor/react": "^4.3.1",
 | 
			
		||||
    "@octokit/core": "^3.5.1",
 | 
			
		||||
    "@radix-ui/colors": "^0.1.7",
 | 
			
		||||
    "@radix-ui/react-alert-dialog": "^0.1.1",
 | 
			
		||||
    "@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-id": "^0.1.1",
 | 
			
		||||
    "@stitches/react": "^1.2.6-0",
 | 
			
		||||
    "base64-js": "^1.5.1",
 | 
			
		||||
    "dinero.js": "^1.9.1",
 | 
			
		||||
    "file-saver": "^2.0.5",
 | 
			
		||||
    "jszip": "^3.7.1",
 | 
			
		||||
    "monaco-editor": "^0.30.1",
 | 
			
		||||
    "next": "^12.0.4",
 | 
			
		||||
    "next-auth": "^4.0.0-beta.5",
 | 
			
		||||
    "next-themes": "^0.0.15",
 | 
			
		||||
    "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",
 | 
			
		||||
    "re-resizable": "^6.9.1",
 | 
			
		||||
    "react": "17.0.2",
 | 
			
		||||
    "react-dom": "17.0.2",
 | 
			
		||||
    "react-hot-keys": "^2.7.1",
 | 
			
		||||
    "react-hot-toast": "^2.1.1",
 | 
			
		||||
    "valtio": "^1.2.5"
 | 
			
		||||
    "react-new-window": "^0.2.1",
 | 
			
		||||
    "react-select": "^5.2.1",
 | 
			
		||||
    "react-split": "^2.0.14",
 | 
			
		||||
    "react-stay-scrolled": "^7.4.0",
 | 
			
		||||
    "reconnecting-websocket": "^4.4.0",
 | 
			
		||||
    "valtio": "^1.2.5",
 | 
			
		||||
    "vscode-languageserver": "^7.0.0",
 | 
			
		||||
    "vscode-uri": "^3.0.2",
 | 
			
		||||
    "wabt": "1.0.16",
 | 
			
		||||
    "xrpl-accountlib": "^1.2.3",
 | 
			
		||||
    "xrpl-client": "^1.9.3"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/dinero.js": "^1.9.0",
 | 
			
		||||
    "@types/file-saver": "^2.0.4",
 | 
			
		||||
    "@types/pako": "^1.0.2",
 | 
			
		||||
    "@types/react": "17.0.31",
 | 
			
		||||
    "eslint": "7.32.0",
 | 
			
		||||
    "eslint-config-next": "11.1.2",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										139
									
								
								pages/_app.tsx
									
									
									
									
									
								
							
							
						
						@@ -1,30 +1,137 @@
 | 
			
		||||
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 { darkTheme } from "../stitches.config";
 | 
			
		||||
import { darkTheme, css } from "../stitches.config";
 | 
			
		||||
import Navigation from "../components/Navigation";
 | 
			
		||||
import { fetchFiles } from "../state/actions";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (gistId && router.isReady) {
 | 
			
		||||
      fetchFiles(gistId);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (!gistId && router.isReady && !router.pathname.includes("/sign-in")) {
 | 
			
		||||
        state.mainModalOpen = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [gistId, router.isReady, router.pathname]);
 | 
			
		||||
 | 
			
		||||
  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 Editor</title>
 | 
			
		||||
        <meta property="og:title" content="XRPL Hooks Editor" />
 | 
			
		||||
        <meta name="twitter:title" content="XRPL Hooks Editor" />
 | 
			
		||||
        <meta name="twitter:card" content="summary_large_image" />
 | 
			
		||||
        <meta name="twitter:site" content="@xrpllabs" />
 | 
			
		||||
        <meta
 | 
			
		||||
          name="description"
 | 
			
		||||
          content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
 | 
			
		||||
        />
 | 
			
		||||
        <meta
 | 
			
		||||
          property="og:description"
 | 
			
		||||
          content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
 | 
			
		||||
        />
 | 
			
		||||
        <meta
 | 
			
		||||
          name="twitter:description"
 | 
			
		||||
          content="Playground for buildings Hooks, that 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 Editor" />
 | 
			
		||||
        <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)"
 | 
			
		||||
        />
 | 
			
		||||
        <link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
        <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"
 | 
			
		||||
        />
 | 
			
		||||
      </Head>
 | 
			
		||||
      <IdProvider>
 | 
			
		||||
        <SessionProvider session={session}>
 | 
			
		||||
          <ThemeProvider
 | 
			
		||||
            attribute="class"
 | 
			
		||||
            defaultTheme="dark"
 | 
			
		||||
            enableSystem={false}
 | 
			
		||||
            value={{
 | 
			
		||||
              light: "light",
 | 
			
		||||
              dark: darkTheme.className,
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Navigation />
 | 
			
		||||
            <Component {...pageProps} />
 | 
			
		||||
            <Toaster
 | 
			
		||||
              toastOptions={{
 | 
			
		||||
                className: css({
 | 
			
		||||
                  backgroundColor: "$mauve1",
 | 
			
		||||
                  color: "$mauve10",
 | 
			
		||||
                  fontSize: "$sm",
 | 
			
		||||
                  zIndex: 9999,
 | 
			
		||||
                  ".dark &": {
 | 
			
		||||
                    backgroundColor: "$mauve4",
 | 
			
		||||
                    color: "$mauve12",
 | 
			
		||||
                  },
 | 
			
		||||
                })(),
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </ThemeProvider>
 | 
			
		||||
        </SessionProvider>
 | 
			
		||||
      </IdProvider>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,21 +16,10 @@ class MyDocument extends Document {
 | 
			
		||||
  }
 | 
			
		||||
  render() {
 | 
			
		||||
    globalStyles();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Html>
 | 
			
		||||
        <Head>
 | 
			
		||||
          <meta name="description" content="Playground for XRPL Hooks" />
 | 
			
		||||
          <link rel="icon" href="/favicon.ico" />
 | 
			
		||||
          <link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
          <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() }}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import type { NextRequest, NextFetchEvent } from 'next/server';
 | 
			
		||||
import { NextResponse as Response } from 'next/server';
 | 
			
		||||
import { getToken } from "next-auth/jwt"
 | 
			
		||||
 | 
			
		||||
export default function middleware(req: NextRequest, ev: NextFetchEvent) {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import NextAuth from "next-auth"
 | 
			
		||||
import GithubProvider from "next-auth/providers/github"
 | 
			
		||||
 | 
			
		||||
export default NextAuth({
 | 
			
		||||
  // Configure one or more authentication providers
 | 
			
		||||
@@ -42,7 +41,6 @@ export default NextAuth({
 | 
			
		||||
    },
 | 
			
		||||
    async session({ session, token }) {
 | 
			
		||||
      session.accessToken = token.accessToken as string;
 | 
			
		||||
      const user = { ...session.user, username: token.username };
 | 
			
		||||
      session['user']['username'] = token.username as string;
 | 
			
		||||
      return session
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								pages/api/faucet.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,36 @@
 | 
			
		||||
// 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!' })
 | 
			
		||||
  }
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await fetch('https://hooks-testnet.xrpl-labs.com/newcreds', { method: 'POST' });
 | 
			
		||||
    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' })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								pages/deploy/[[...slug]].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,63 @@
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										83
									
								
								pages/develop/[[...slug]].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,83 @@
 | 
			
		||||
import type { NextPage } from "next";
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
import { Play } from "phosphor-react";
 | 
			
		||||
import Hotkeys from "react-hot-keys";
 | 
			
		||||
import Split from "react-split";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import Box from "../../components/Box";
 | 
			
		||||
import Button from "../../components/Button";
 | 
			
		||||
import state from "../../state";
 | 
			
		||||
import { compileCode } from "../../state/actions";
 | 
			
		||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const LogBox = dynamic(() => import("../../components/LogBox"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const Home: NextPage = () => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
 | 
			
		||||
  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 />
 | 
			
		||||
        {snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() ===
 | 
			
		||||
          "c" && (
 | 
			
		||||
          <Hotkeys
 | 
			
		||||
            keyName="command+b,ctrl+b"
 | 
			
		||||
            onKeyDown={() =>
 | 
			
		||||
              !snap.compiling && snap.files.length && compileCode(snap.active)
 | 
			
		||||
            }
 | 
			
		||||
          >
 | 
			
		||||
            <Button
 | 
			
		||||
              variant="primary"
 | 
			
		||||
              uppercase
 | 
			
		||||
              disabled={!snap.files.length}
 | 
			
		||||
              isLoading={snap.compiling}
 | 
			
		||||
              onClick={() => compileCode(snap.active)}
 | 
			
		||||
              css={{
 | 
			
		||||
                position: "absolute",
 | 
			
		||||
                bottom: "$4",
 | 
			
		||||
                left: "$4",
 | 
			
		||||
                alignItems: "center",
 | 
			
		||||
                display: "flex",
 | 
			
		||||
                cursor: "pointer",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Play weight="bold" size="16px" />
 | 
			
		||||
              Compile to Wasm
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Hotkeys>
 | 
			
		||||
        )}
 | 
			
		||||
      </main>
 | 
			
		||||
      <Box
 | 
			
		||||
        css={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          background: "$mauve1",
 | 
			
		||||
          position: "relative",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <LogBox
 | 
			
		||||
          title="Development Log"
 | 
			
		||||
          clearLog={() => (state.logs = [])}
 | 
			
		||||
          logs={snap.logs}
 | 
			
		||||
        />
 | 
			
		||||
      </Box>
 | 
			
		||||
    </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;
 | 
			
		||||
							
								
								
									
										38
									
								
								pages/sign-in.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										442
									
								
								pages/test/[[...slug]].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,442 @@
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
import { Play } from "phosphor-react";
 | 
			
		||||
import { FC, useCallback, useEffect, useState } from "react";
 | 
			
		||||
import Split from "react-split";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import {
 | 
			
		||||
  Box, Button, Container,
 | 
			
		||||
  Flex, Input,
 | 
			
		||||
  Select, Tab, Tabs, Text
 | 
			
		||||
} from "../../components";
 | 
			
		||||
import transactionsData from "../../content/transactions.json";
 | 
			
		||||
import state from "../../state";
 | 
			
		||||
import { sendTransaction } from "../../state/actions";
 | 
			
		||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
 | 
			
		||||
 | 
			
		||||
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const LogBox = dynamic(() => import("../../components/LogBox"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
});
 | 
			
		||||
const Accounts = dynamic(() => import("../../components/Accounts"), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// type SelectOption<T> = { value: T, label: string };
 | 
			
		||||
type TxFields = Omit<
 | 
			
		||||
  typeof transactionsData[0],
 | 
			
		||||
  "Account" | "Sequence" | "TransactionType"
 | 
			
		||||
>;
 | 
			
		||||
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  header?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Transaction: FC<Props> = ({ header, ...props }) => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
 | 
			
		||||
  const transactionsOptions = transactionsData.map((tx) => ({
 | 
			
		||||
    value: tx.TransactionType,
 | 
			
		||||
    label: tx.TransactionType,
 | 
			
		||||
  }));
 | 
			
		||||
  const [selectedTransaction, setSelectedTransaction] = useState<
 | 
			
		||||
    typeof transactionsOptions[0] | null
 | 
			
		||||
  >(null);
 | 
			
		||||
 | 
			
		||||
  const accountOptions = snap.accounts.map((acc) => ({
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
    value: acc.address,
 | 
			
		||||
  }));
 | 
			
		||||
  const [selectedAccount, setSelectedAccount] = useState<
 | 
			
		||||
    typeof accountOptions[0] | null
 | 
			
		||||
  >(null);
 | 
			
		||||
 | 
			
		||||
  const destAccountOptions = snap.accounts
 | 
			
		||||
    .map((acc) => ({
 | 
			
		||||
      label: acc.name,
 | 
			
		||||
      value: acc.address,
 | 
			
		||||
    }))
 | 
			
		||||
    .filter((acc) => acc.value !== selectedAccount?.value);
 | 
			
		||||
  const [selectedDestAccount, setSelectedDestAccount] = useState<
 | 
			
		||||
    typeof destAccountOptions[0] | null
 | 
			
		||||
  >(null);
 | 
			
		||||
 | 
			
		||||
  const [txIsLoading, setTxIsLoading] = useState(false);
 | 
			
		||||
  const [txIsDisabled, setTxIsDisabled] = useState(false);
 | 
			
		||||
  const [txFields, setTxFields] = useState<TxFields>({});
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const transactionType = selectedTransaction?.value;
 | 
			
		||||
    const account = snap.accounts.find(
 | 
			
		||||
      (acc) => acc.address === selectedAccount?.value
 | 
			
		||||
    );
 | 
			
		||||
    if (!account || !transactionType || txIsLoading) {
 | 
			
		||||
      setTxIsDisabled(true);
 | 
			
		||||
    } else {
 | 
			
		||||
      setTxIsDisabled(false);
 | 
			
		||||
    }
 | 
			
		||||
  }, [txIsLoading, selectedTransaction, selectedAccount, snap.accounts]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let _txFields: TxFields | undefined = transactionsData.find(
 | 
			
		||||
      (tx) => tx.TransactionType === selectedTransaction?.value
 | 
			
		||||
    );
 | 
			
		||||
    if (!_txFields) return setTxFields({});
 | 
			
		||||
    _txFields = { ..._txFields } as TxFields;
 | 
			
		||||
 | 
			
		||||
    setSelectedDestAccount(null);
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    delete _txFields.TransactionType;
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    delete _txFields.Account;
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    delete _txFields.Sequence;
 | 
			
		||||
    setTxFields(_txFields);
 | 
			
		||||
  }, [selectedTransaction, setSelectedDestAccount]);
 | 
			
		||||
 | 
			
		||||
  const submitTest = useCallback(async () => {
 | 
			
		||||
    const account = snap.accounts.find(
 | 
			
		||||
      (acc) => acc.address === selectedAccount?.value
 | 
			
		||||
    );
 | 
			
		||||
    const TransactionType = selectedTransaction?.value;
 | 
			
		||||
    if (!account || !TransactionType || txIsDisabled) return;
 | 
			
		||||
 | 
			
		||||
    setTxIsLoading(true);
 | 
			
		||||
    // setTxIsError(null)
 | 
			
		||||
    try {
 | 
			
		||||
      let options = { ...txFields };
 | 
			
		||||
 | 
			
		||||
      options.Destination = selectedDestAccount?.value;
 | 
			
		||||
      (Object.keys(options) as (keyof TxFields)[]).forEach((field) => {
 | 
			
		||||
        let _value = options[field];
 | 
			
		||||
        // convert currency
 | 
			
		||||
        if (typeof _value === "object" && _value.type === "currency") {
 | 
			
		||||
          if (+_value.value) {
 | 
			
		||||
            options[field] = (+_value.value * 1000000 + "") as any;
 | 
			
		||||
          } else {
 | 
			
		||||
            options[field] = undefined; // 👇 💀
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        // handle type: `json`
 | 
			
		||||
        if (typeof _value === "object" && _value.type === "json") {
 | 
			
		||||
          if (typeof _value.value === "object") {
 | 
			
		||||
            options[field] = _value.value as any;
 | 
			
		||||
          } else {
 | 
			
		||||
            try {
 | 
			
		||||
              options[field] = JSON.parse(_value.value);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
              const message = `Input error for json field '${field}': ${
 | 
			
		||||
                error instanceof Error ? error.message : ""
 | 
			
		||||
              }`;
 | 
			
		||||
              throw Error(message);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // delete unneccesary fields
 | 
			
		||||
        if (!options[field]) {
 | 
			
		||||
          delete options[field];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
 | 
			
		||||
      await sendTransaction(
 | 
			
		||||
        account,
 | 
			
		||||
        {
 | 
			
		||||
          TransactionType,
 | 
			
		||||
          ...options,
 | 
			
		||||
        },
 | 
			
		||||
        { logPrefix }
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(error);
 | 
			
		||||
      if (error instanceof Error) {
 | 
			
		||||
        state.transactionLogs.push({ type: "error", message: error.message });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    setTxIsLoading(false);
 | 
			
		||||
  }, [
 | 
			
		||||
    header,
 | 
			
		||||
    selectedAccount?.value,
 | 
			
		||||
    selectedDestAccount?.value,
 | 
			
		||||
    selectedTransaction?.value,
 | 
			
		||||
    snap.accounts,
 | 
			
		||||
    txFields,
 | 
			
		||||
    txIsDisabled,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  const resetState = useCallback(() => {
 | 
			
		||||
    setSelectedAccount(null);
 | 
			
		||||
    setSelectedDestAccount(null);
 | 
			
		||||
    setSelectedTransaction(null);
 | 
			
		||||
    setTxFields({});
 | 
			
		||||
    setTxIsDisabled(false);
 | 
			
		||||
    setTxIsLoading(false);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
 | 
			
		||||
  const otherFields = Object.keys(txFields).filter(
 | 
			
		||||
    (k) => !usualFields.includes(k)
 | 
			
		||||
  ) as OtherFields;
 | 
			
		||||
  return (
 | 
			
		||||
    <Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
 | 
			
		||||
      <Container
 | 
			
		||||
        css={{ p: "$3 0", fontSize: "$sm", height: "calc(100% - 28px)" }}
 | 
			
		||||
      >
 | 
			
		||||
        <Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            fluid
 | 
			
		||||
            css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}
 | 
			
		||||
          >
 | 
			
		||||
            <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
              Transaction type:{" "}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Select
 | 
			
		||||
              instanceId="transactionsType"
 | 
			
		||||
              placeholder="Select transaction type"
 | 
			
		||||
              options={transactionsOptions}
 | 
			
		||||
              hideSelectedOptions
 | 
			
		||||
              css={{ width: "70%" }}
 | 
			
		||||
              value={selectedTransaction}
 | 
			
		||||
              onChange={(tt) => setSelectedTransaction(tt as any)}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            fluid
 | 
			
		||||
            css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}
 | 
			
		||||
          >
 | 
			
		||||
            <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
              Account:{" "}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Select
 | 
			
		||||
              instanceId="from-account"
 | 
			
		||||
              placeholder="Select your account"
 | 
			
		||||
              css={{ width: "70%" }}
 | 
			
		||||
              options={accountOptions}
 | 
			
		||||
              value={selectedAccount}
 | 
			
		||||
              onChange={(acc) => setSelectedAccount(acc as any)}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
          {txFields.Amount !== undefined && (
 | 
			
		||||
            <Flex
 | 
			
		||||
              row
 | 
			
		||||
              fluid
 | 
			
		||||
              css={{
 | 
			
		||||
                justifyContent: "flex-end",
 | 
			
		||||
                alignItems: "center",
 | 
			
		||||
                mb: "$3",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
                Amount (XRP):{" "}
 | 
			
		||||
              </Text>
 | 
			
		||||
              <Input
 | 
			
		||||
                value={txFields.Amount.value}
 | 
			
		||||
                onChange={(e) =>
 | 
			
		||||
                  setTxFields({
 | 
			
		||||
                    ...txFields,
 | 
			
		||||
                    Amount: { type: "currency", value: e.target.value },
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
                variant="deep"
 | 
			
		||||
                css={{ width: "70%", flex: "inherit", height: "$9" }}
 | 
			
		||||
              />
 | 
			
		||||
            </Flex>
 | 
			
		||||
          )}
 | 
			
		||||
          {txFields.Destination !== undefined && (
 | 
			
		||||
            <Flex
 | 
			
		||||
              row
 | 
			
		||||
              fluid
 | 
			
		||||
              css={{
 | 
			
		||||
                justifyContent: "flex-end",
 | 
			
		||||
                alignItems: "center",
 | 
			
		||||
                mb: "$3",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
                Destination account:{" "}
 | 
			
		||||
              </Text>
 | 
			
		||||
              <Select
 | 
			
		||||
                instanceId="to-account"
 | 
			
		||||
                placeholder="Select the destination account"
 | 
			
		||||
                css={{ width: "70%" }}
 | 
			
		||||
                options={destAccountOptions}
 | 
			
		||||
                value={selectedDestAccount}
 | 
			
		||||
                isClearable
 | 
			
		||||
                onChange={(acc) => setSelectedDestAccount(acc as any)}
 | 
			
		||||
              />
 | 
			
		||||
            </Flex>
 | 
			
		||||
          )}
 | 
			
		||||
          {otherFields.map((field) => {
 | 
			
		||||
            let _value = txFields[field];
 | 
			
		||||
            let value = typeof _value === "object" ? _value.value : _value;
 | 
			
		||||
            value =
 | 
			
		||||
              typeof value === "object"
 | 
			
		||||
                ? JSON.stringify(value)
 | 
			
		||||
                : value?.toLocaleString();
 | 
			
		||||
            let isCurrency =
 | 
			
		||||
              typeof _value === "object" && _value.type === "currency";
 | 
			
		||||
            return (
 | 
			
		||||
              <Flex
 | 
			
		||||
                key={field}
 | 
			
		||||
                row
 | 
			
		||||
                fluid
 | 
			
		||||
                css={{
 | 
			
		||||
                  justifyContent: "flex-end",
 | 
			
		||||
                  alignItems: "center",
 | 
			
		||||
                  mb: "$3",
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text muted css={{ mr: "$3" }}>
 | 
			
		||||
                  {field + (isCurrency ? " (XRP)" : "")}:{" "}
 | 
			
		||||
                </Text>
 | 
			
		||||
                <Input
 | 
			
		||||
                  value={value}
 | 
			
		||||
                  onChange={(e) =>
 | 
			
		||||
                    setTxFields({
 | 
			
		||||
                      ...txFields,
 | 
			
		||||
                      [field]:
 | 
			
		||||
                        typeof _value === "object"
 | 
			
		||||
                          ? { ..._value, value: e.target.value }
 | 
			
		||||
                          : e.target.value,
 | 
			
		||||
                    })
 | 
			
		||||
                  }
 | 
			
		||||
                  variant="deep"
 | 
			
		||||
                  css={{ width: "70%", flex: "inherit", height: "$9" }}
 | 
			
		||||
                />
 | 
			
		||||
              </Flex>
 | 
			
		||||
            );
 | 
			
		||||
          })}
 | 
			
		||||
        </Flex>
 | 
			
		||||
      </Container>
 | 
			
		||||
      <Flex
 | 
			
		||||
        row
 | 
			
		||||
        css={{
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
          position: "absolute",
 | 
			
		||||
          left: 0,
 | 
			
		||||
          bottom: 0,
 | 
			
		||||
          width: "100%",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Button outline>VIEW AS JSON</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>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Test = () => {
 | 
			
		||||
  const { transactionLogs } = useSnapshot(state);
 | 
			
		||||
  const [tabHeaders, setTabHeaders] = useState<string[]>(["test1.json"]);
 | 
			
		||||
  return (
 | 
			
		||||
    <Container css={{ px: 0 }}>
 | 
			
		||||
      <Split
 | 
			
		||||
        direction="vertical"
 | 
			
		||||
        sizes={getSplit("testVertical") || [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
 | 
			
		||||
                keepAllAlive
 | 
			
		||||
                forceDefaultExtension
 | 
			
		||||
                defaultExtension=".json"
 | 
			
		||||
                onCreateNewTab={(name) =>
 | 
			
		||||
                  setTabHeaders(tabHeaders.concat(name))
 | 
			
		||||
                }
 | 
			
		||||
                onCloseTab={(index) =>
 | 
			
		||||
                  setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                {tabHeaders.map((header) => (
 | 
			
		||||
                  <Tab key={header} header={header}>
 | 
			
		||||
                    <Transaction header={header} />
 | 
			
		||||
                  </Tab>
 | 
			
		||||
                ))}
 | 
			
		||||
              </Tabs>
 | 
			
		||||
            </Box>
 | 
			
		||||
            <Box css={{ width: "45%", mx: "$2", height: "100%" }}>
 | 
			
		||||
              <Accounts card hideDeployBtn showHookStats />
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Split>
 | 
			
		||||
        </Flex>
 | 
			
		||||
 | 
			
		||||
        <Flex row fluid>
 | 
			
		||||
          <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;
 | 
			
		||||
							
								
								
									
										419
									
								
								patches/ripple-binary-codec+1.2.0.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,419 @@
 | 
			
		||||
diff --git a/node_modules/ripple-binary-codec/dist/enums/definitions.json b/node_modules/ripple-binary-codec/dist/enums/definitions.json
 | 
			
		||||
index 2333c42..b8f8eab 100644
 | 
			
		||||
--- a/node_modules/ripple-binary-codec/dist/enums/definitions.json
 | 
			
		||||
+++ b/node_modules/ripple-binary-codec/dist/enums/definitions.json
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
+
 | 
			
		||||
 {
 | 
			
		||||
   "TYPES": {
 | 
			
		||||
     "Validation": 10003,
 | 
			
		||||
@@ -40,9 +41,7 @@
 | 
			
		||||
     "Check": 67,
 | 
			
		||||
     "Nickname": 110,
 | 
			
		||||
     "Contract": 99,
 | 
			
		||||
-    "NFTokenPage": 80,
 | 
			
		||||
-    "NFTokenOffer": 55,
 | 
			
		||||
-    "NegativeUNL": 78
 | 
			
		||||
+    "GeneratorMap": 103
 | 
			
		||||
   },
 | 
			
		||||
   "FIELDS": [
 | 
			
		||||
     [
 | 
			
		||||
@@ -95,16 +94,6 @@
 | 
			
		||||
         "type": "UInt16"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "TransferFee",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 4,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt16"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "Flags",
 | 
			
		||||
       {
 | 
			
		||||
@@ -455,6 +444,16 @@
 | 
			
		||||
         "type": "UInt32"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
+    [
 | 
			
		||||
+      "EmitGeneration",
 | 
			
		||||
+      {
 | 
			
		||||
+        "nth": 43,
 | 
			
		||||
+        "isVLEncoded": false,
 | 
			
		||||
+        "isSerialized": true,
 | 
			
		||||
+        "isSigningField": true,
 | 
			
		||||
+        "type": "UInt32"
 | 
			
		||||
+      }
 | 
			
		||||
+    ],
 | 
			
		||||
     [
 | 
			
		||||
       "IndexNext",
 | 
			
		||||
       {
 | 
			
		||||
@@ -635,16 +634,6 @@
 | 
			
		||||
         "type": "Hash256"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "TokenID",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 10,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Hash256"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "BookDirectory",
 | 
			
		||||
       {
 | 
			
		||||
@@ -916,7 +905,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "URI",
 | 
			
		||||
+      "Generator",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 5,
 | 
			
		||||
         "isVLEncoded": true,
 | 
			
		||||
@@ -1045,36 +1034,6 @@
 | 
			
		||||
         "type": "Blob"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "UNLModifyValidator",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 19,
 | 
			
		||||
-        "isVLEncoded": true,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Blob"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "ValidatorToDisable",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 20,
 | 
			
		||||
-        "isVLEncoded": true,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Blob"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "ValidatorToReEnable",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 21,
 | 
			
		||||
-        "isVLEncoded": true,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Blob"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "Account",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1156,7 +1115,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "Minter",
 | 
			
		||||
+      "EmitCallback",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 9,
 | 
			
		||||
         "isVLEncoded": true,
 | 
			
		||||
@@ -1276,9 +1235,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "NonFungibleToken",
 | 
			
		||||
+      "Signer",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 12,
 | 
			
		||||
+        "nth": 16,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1286,9 +1245,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "Signer",
 | 
			
		||||
+      "Majority",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 16,
 | 
			
		||||
+        "nth": 18,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1296,9 +1255,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "Majority",
 | 
			
		||||
+      "DisabledValidator",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 18,
 | 
			
		||||
+        "nth": 19,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1306,9 +1265,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "DisabledValidator",
 | 
			
		||||
+      "EmitDetails",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 19,
 | 
			
		||||
+        "nth": 12,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1395,16 +1354,6 @@
 | 
			
		||||
         "type": "STArray"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "NonFungibleTokens",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 10,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "STArray"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "Majorities",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1415,16 +1364,6 @@
 | 
			
		||||
         "type": "STArray"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "DisabledValidators",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 17,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "STArray"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "CloseResolution",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1535,16 +1474,6 @@
 | 
			
		||||
         "type": "Vector256"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "TokenIDs",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 4,
 | 
			
		||||
-        "isVLEncoded": true,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Vector256"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "Transaction",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1596,7 +1525,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "TicketCount",
 | 
			
		||||
+      "HookStateCount",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 40,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
@@ -1606,7 +1535,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "TicketSequence",
 | 
			
		||||
+      "HookReserveCount",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 41,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
@@ -1616,7 +1545,7 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "TokenTaxon",
 | 
			
		||||
+      "HookDataMaxSize",
 | 
			
		||||
       {
 | 
			
		||||
         "nth": 42,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
@@ -1626,23 +1555,23 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "MintedTokens",
 | 
			
		||||
+      "HookOn",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 43,
 | 
			
		||||
+        "nth": 16,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
-        "type": "UInt32"
 | 
			
		||||
+        "type": "UInt64"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "BurnedTokens",
 | 
			
		||||
+      "EmitBurden",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 44,
 | 
			
		||||
+        "nth": 12,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
-        "type": "UInt32"
 | 
			
		||||
+        "type": "UInt64"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
@@ -1686,29 +1615,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "PreviousPageMin",
 | 
			
		||||
+      "EmitParentTxnID",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 26,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Hash256"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "NextPageMin",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 27,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "Hash256"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "BuyOffer",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 28,
 | 
			
		||||
+        "nth": 10,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1716,9 +1625,9 @@
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
     [
 | 
			
		||||
-      "SellOffer",
 | 
			
		||||
+      "EmitNonce",
 | 
			
		||||
       {
 | 
			
		||||
-        "nth": 29,
 | 
			
		||||
+        "nth": 11,
 | 
			
		||||
         "isVLEncoded": false,
 | 
			
		||||
         "isSerialized": true,
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
@@ -1735,16 +1644,6 @@
 | 
			
		||||
         "type": "UInt8"
 | 
			
		||||
       }
 | 
			
		||||
     ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "UNLModifyDisabling",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 17,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt8"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
     [
 | 
			
		||||
       "DestinationNode",
 | 
			
		||||
       {
 | 
			
		||||
@@ -1754,36 +1653,6 @@
 | 
			
		||||
         "isSigningField": true,
 | 
			
		||||
         "type": "UInt64"
 | 
			
		||||
       }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "Cookie",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 10,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt64"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "ServerVersion",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 11,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt64"
 | 
			
		||||
-      }
 | 
			
		||||
-    ],
 | 
			
		||||
-    [
 | 
			
		||||
-      "OfferNode",
 | 
			
		||||
-      {
 | 
			
		||||
-        "nth": 12,
 | 
			
		||||
-        "isVLEncoded": false,
 | 
			
		||||
-        "isSerialized": true,
 | 
			
		||||
-        "isSigningField": true,
 | 
			
		||||
-        "type": "UInt64"
 | 
			
		||||
-      }
 | 
			
		||||
     ]
 | 
			
		||||
   ],
 | 
			
		||||
   "TRANSACTION_RESULTS": {
 | 
			
		||||
@@ -1908,18 +1777,7 @@
 | 
			
		||||
     "tecDUPLICATE": 149,
 | 
			
		||||
     "tecKILLED": 150,
 | 
			
		||||
     "tecHAS_OBLIGATIONS": 151,
 | 
			
		||||
-    "tecTOO_SOON": 152,
 | 
			
		||||
-
 | 
			
		||||
-    "tecMAX_SEQUENCE_REACHED": 154,
 | 
			
		||||
-    "tecNO_SUITABLE_PAGE": 155,
 | 
			
		||||
-    "tecBUY_SELL_MISMATCH": 156,
 | 
			
		||||
-    "tecOFFER_TYPE_MISMATCH": 157,
 | 
			
		||||
-    "tecCANT_ACCEPT_OWN_OFFER": 158,
 | 
			
		||||
-    "tecINSUFFICIENT_FUNDS": 159,
 | 
			
		||||
-    "tecOBJECT_NOT_FOUND": 160,
 | 
			
		||||
-    "tecINSUFFICIENT_PAYMENT": 161,
 | 
			
		||||
-    "tecINCORRECT_ASSET": 162,
 | 
			
		||||
-    "tecTOO_MANY": 163
 | 
			
		||||
+    "tecTOO_SOON": 152
 | 
			
		||||
   },
 | 
			
		||||
   "TRANSACTION_TYPES": {
 | 
			
		||||
     "Invalid": -1,
 | 
			
		||||
@@ -1946,13 +1804,11 @@
 | 
			
		||||
     "DepositPreauth": 19,
 | 
			
		||||
     "TrustSet": 20,
 | 
			
		||||
     "AccountDelete": 21,
 | 
			
		||||
-    "NFTokenMint": 25,
 | 
			
		||||
-    "NFTokenBurn": 26,
 | 
			
		||||
-    "NFTokenCreateOffer": 27,
 | 
			
		||||
-    "NFTokenCancelOffer": 28,
 | 
			
		||||
-    "NFTokenAcceptOffer": 29,
 | 
			
		||||
+    "SetHook": 22,
 | 
			
		||||
+    "Invoke": 23,
 | 
			
		||||
+    "Batch": 24,
 | 
			
		||||
+
 | 
			
		||||
     "EnableAmendment": 100,
 | 
			
		||||
-    "SetFee": 101,
 | 
			
		||||
-    "UNLModify": 102
 | 
			
		||||
+    "SetFee": 101
 | 
			
		||||
   }
 | 
			
		||||
 }
 | 
			
		||||
							
								
								
									
										
											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  | 
							
								
								
									
										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))
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										76
									
								
								state/actions/addFaucetAccount.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,76 @@
 | 
			
		||||
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import state, { FaucetAccountRes } from '../index';
 | 
			
		||||
 | 
			
		||||
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 (showToast: boolean = false) => {
 | 
			
		||||
  // Lets limit the number of faucet accounts to 5 for now
 | 
			
		||||
  if (state.accounts.length > 4) {
 | 
			
		||||
    return toast.error("You can only have maximum 5 accounts");
 | 
			
		||||
  }
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    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 toast.error(json.error, { id: toastId });
 | 
			
		||||
      } else {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (showToast) {
 | 
			
		||||
        toast.success("New account created", { id: toastId });
 | 
			
		||||
      }
 | 
			
		||||
      state.accounts.push({
 | 
			
		||||
        name: names[state.accounts.length],
 | 
			
		||||
        xrp: (json.xrp || 0 * 1000000).toString(),
 | 
			
		||||
        address: json.address,
 | 
			
		||||
        secret: json.secret,
 | 
			
		||||
        sequence: 1,
 | 
			
		||||
        hooks: [],
 | 
			
		||||
        isLoading: false,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// fetch initial faucets
 | 
			
		||||
(async function fetchFaucets() {
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
    if (state.accounts.length < 2) {
 | 
			
		||||
      await addFaucetAccount();
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        addFaucetAccount();
 | 
			
		||||
      }, 10000);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})();
 | 
			
		||||
							
								
								
									
										90
									
								
								state/actions/compileCode.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,90 @@
 | 
			
		||||
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);
 | 
			
		||||
  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
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Set loading state to true
 | 
			
		||||
  state.compiling = true;
 | 
			
		||||
  state.logs = []
 | 
			
		||||
  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,
 | 
			
		||||
            src: state.files[activeId].content,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      }),
 | 
			
		||||
    });
 | 
			
		||||
    const json = await res.json();
 | 
			
		||||
    state.compiling = false;
 | 
			
		||||
    if (!json.success) {
 | 
			
		||||
      state.logs.push({ type: "error", message: json.message });
 | 
			
		||||
      if (json.tasks && json.tasks.length > 0) {
 | 
			
		||||
        json.tasks.forEach((task: any) => {
 | 
			
		||||
          if (!task.success) {
 | 
			
		||||
            state.logs.push({ type: "error", message: task?.console });
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return toast.error(`Couldn't compile!`, { 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",
 | 
			
		||||
    });
 | 
			
		||||
    // Decode base64 encoded wasm that is coming back from the endpoint
 | 
			
		||||
    const bufferData = await decodeBinary(json.output);
 | 
			
		||||
    state.files[state.active].compiledContent = ref(bufferData);
 | 
			
		||||
    // Import wabt from and create human readable version of wasm file and
 | 
			
		||||
    // put it into state
 | 
			
		||||
    import("wabt").then((wabt) => {
 | 
			
		||||
      const ww = wabt.default();
 | 
			
		||||
      const myModule = ww.readWasm(new Uint8Array(bufferData), {
 | 
			
		||||
        readDebugNames: true,
 | 
			
		||||
      });
 | 
			
		||||
      myModule.applyNames();
 | 
			
		||||
 | 
			
		||||
      const wast = myModule.toText({ foldExprs: false, inlineExport: false });
 | 
			
		||||
      state.files[state.active].compiledWatContent = wast;
 | 
			
		||||
      toast.success("Compiled successfully!", { position: "bottom-center" });
 | 
			
		||||
    });
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.log(err);
 | 
			
		||||
    state.logs.push({
 | 
			
		||||
      type: "error",
 | 
			
		||||
      message: "Error occured while compiling!",
 | 
			
		||||
    });
 | 
			
		||||
    state.compiling = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										8
									
								
								state/actions/createNewFile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
			
		||||
import state, { IFile } from '../index';
 | 
			
		||||
 | 
			
		||||
/* Initializes empty file to global state */
 | 
			
		||||
export const createNewFile = (name: string) => {
 | 
			
		||||
  const emptyFile: IFile = { name, language: "c", content: "" };
 | 
			
		||||
  state.files.push(emptyFile);
 | 
			
		||||
  state.active = state.files.length - 1;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										98
									
								
								state/actions/deployHook.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,98 @@
 | 
			
		||||
import { derive, sign } from "xrpl-accountlib";
 | 
			
		||||
 | 
			
		||||
import state, { IAccount } from "../index";
 | 
			
		||||
 | 
			
		||||
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
 | 
			
		||||
  if (!arrayBuffer) {
 | 
			
		||||
    return "";
 | 
			
		||||
  }
 | 
			
		||||
  if (
 | 
			
		||||
    typeof arrayBuffer !== "object" ||
 | 
			
		||||
    arrayBuffer === null ||
 | 
			
		||||
    typeof arrayBuffer.byteLength !== "number"
 | 
			
		||||
  ) {
 | 
			
		||||
    throw new TypeError("Expected input to be an ArrayBuffer");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var view = new Uint8Array(arrayBuffer);
 | 
			
		||||
  var result = "";
 | 
			
		||||
  var value;
 | 
			
		||||
 | 
			
		||||
  for (var i = 0; i < view.length; i++) {
 | 
			
		||||
    value = view[i].toString(16);
 | 
			
		||||
    result += value.length === 1 ? "0" + value : value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* deployHook function turns the wasm binary into
 | 
			
		||||
 * hex string, signs the transaction and deploys it to
 | 
			
		||||
 * Hooks testnet.
 | 
			
		||||
 */
 | 
			
		||||
export const deployHook = async (account: IAccount & { name?: string }) => {
 | 
			
		||||
  if (
 | 
			
		||||
    !state.files ||
 | 
			
		||||
    state.files.length === 0 ||
 | 
			
		||||
    !state.files?.[state.active]?.compiledContent
 | 
			
		||||
  ) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!state.files?.[state.active]?.compiledContent) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!state.client) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (typeof window !== "undefined") {
 | 
			
		||||
    const tx = {
 | 
			
		||||
      Account: account.address,
 | 
			
		||||
      TransactionType: "SetHook",
 | 
			
		||||
      CreateCode: arrayBufferToHex(
 | 
			
		||||
        state.files?.[state.active]?.compiledContent
 | 
			
		||||
      ).toUpperCase(),
 | 
			
		||||
      HookOn: "0000000000000000",
 | 
			
		||||
      Sequence: account.sequence,
 | 
			
		||||
      Fee: "1000",
 | 
			
		||||
    };
 | 
			
		||||
    const keypair = derive.familySeed(account.secret);
 | 
			
		||||
    const { signedTransaction } = sign(tx, keypair);
 | 
			
		||||
    const currentAccount = state.accounts.find(
 | 
			
		||||
      (acc) => acc.address === account.address
 | 
			
		||||
    );
 | 
			
		||||
    if (currentAccount) {
 | 
			
		||||
      currentAccount.isLoading = true;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const submitRes = await state.client.send({
 | 
			
		||||
        command: "submit",
 | 
			
		||||
        tx_blob: signedTransaction,
 | 
			
		||||
      });
 | 
			
		||||
      if (submitRes.engine_result === "tesSUCCESS") {
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: "success",
 | 
			
		||||
          message: "Hook deployed successfully ✅",
 | 
			
		||||
        });
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: "success",
 | 
			
		||||
          message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: "error",
 | 
			
		||||
          message: `[${submitRes.engine_result}] ${submitRes.engine_result_message}`,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.log(err);
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: "error",
 | 
			
		||||
        message: "Error occured while deploying",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (currentAccount) {
 | 
			
		||||
      currentAccount.isLoading = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										19
									
								
								state/actions/downloadAsZip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
import { createZip } from '../../utils/zip';
 | 
			
		||||
import { guessZipFileName } from '../../utils/helpers';
 | 
			
		||||
import state from '..'
 | 
			
		||||
import toast from 'react-hot-toast';
 | 
			
		||||
 | 
			
		||||
export const downloadAsZip = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        state.zipLoading = true
 | 
			
		||||
        // TODO do something about file/gist loading state
 | 
			
		||||
        const files = state.files.map(({ name, content }) => ({ name, content }));
 | 
			
		||||
        const zipped = await createZip(files);
 | 
			
		||||
        const zipFileName = guessZipFileName(files);
 | 
			
		||||
        zipped.saveFile(zipFileName);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        toast.error('Error occured while creating zip file, try again later')
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.zipLoading = false
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										71
									
								
								state/actions/fetchFiles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,71 @@
 | 
			
		||||
import { Octokit } from "@octokit/core";
 | 
			
		||||
import Router from "next/router";
 | 
			
		||||
import state from '../index';
 | 
			
		||||
import { templateFileIds } from '../constants';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const octokit = new Octokit();
 | 
			
		||||
 | 
			
		||||
/* Fetches Gist files from Githug Gists based on
 | 
			
		||||
 * gistId and stores the content in global state
 | 
			
		||||
 */
 | 
			
		||||
export const fetchFiles = (gistId: string) => {
 | 
			
		||||
  state.loading = true;
 | 
			
		||||
  if (gistId && !state.files.length) {
 | 
			
		||||
    state.logs.push({
 | 
			
		||||
      type: "log",
 | 
			
		||||
      message: `Fetching Gist with id: ${gistId}`,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    octokit
 | 
			
		||||
      .request("GET /gists/{gist_id}", { gist_id: gistId })
 | 
			
		||||
      .then(res => {
 | 
			
		||||
        if (!Object.values(templateFileIds).includes(gistId)) {
 | 
			
		||||
          return res
 | 
			
		||||
        }
 | 
			
		||||
        // in case of templates, fetch header file(s) and append to res
 | 
			
		||||
        return octokit.request("GET /gists/{gist_id}", { gist_id: templateFileIds.headers }).then(({ data: { files: headerFiles } }) => {
 | 
			
		||||
          const files = { ...res.data.files, ...headerFiles }
 | 
			
		||||
          res.data.files = files
 | 
			
		||||
          return res
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
      .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: "success",
 | 
			
		||||
              message: "Fetched successfully ✅",
 | 
			
		||||
            });
 | 
			
		||||
            state.files = files;
 | 
			
		||||
            state.gistId = gistId;
 | 
			
		||||
            state.gistName = Object.keys(res.data.files)?.[0] || "untitled";
 | 
			
		||||
            state.gistOwner = res.data.owner?.login;
 | 
			
		||||
            return;
 | 
			
		||||
          } else {
 | 
			
		||||
            // Open main modal if now files
 | 
			
		||||
            state.mainModalOpen = true;
 | 
			
		||||
          }
 | 
			
		||||
          return Router.push({ pathname: "/develop" });
 | 
			
		||||
        }
 | 
			
		||||
        state.loading = false;
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        // console.error(err)
 | 
			
		||||
        state.loading = false;
 | 
			
		||||
        state.logs.push({
 | 
			
		||||
          type: "error",
 | 
			
		||||
          message: `Couldn't find Gist with id: ${gistId}`,
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      });
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  state.loading = false;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										29
									
								
								state/actions/importAccount.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import { derive } from "xrpl-accountlib";
 | 
			
		||||
 | 
			
		||||
import state from '../index';
 | 
			
		||||
import { names } from './addFaucetAccount';
 | 
			
		||||
 | 
			
		||||
// Adds test account to global state with secret key
 | 
			
		||||
export const importAccount = (secret: string) => {
 | 
			
		||||
  if (!secret) {
 | 
			
		||||
    return toast.error("You need to add secret!");
 | 
			
		||||
  }
 | 
			
		||||
  if (state.accounts.find((acc) => acc.secret === secret)) {
 | 
			
		||||
    return toast.error("Account already added!");
 | 
			
		||||
  }
 | 
			
		||||
  const account = derive.familySeed(secret);
 | 
			
		||||
  if (!account.secret.familySeed) {
 | 
			
		||||
    return toast.error(`Couldn't create account!`);
 | 
			
		||||
  }
 | 
			
		||||
  state.accounts.push({
 | 
			
		||||
    name: names[state.accounts.length],
 | 
			
		||||
    address: account.address || "",
 | 
			
		||||
    secret: account.secret.familySeed || "",
 | 
			
		||||
    xrp: "0",
 | 
			
		||||
    sequence: 1,
 | 
			
		||||
    hooks: [],
 | 
			
		||||
    isLoading: false,
 | 
			
		||||
  });
 | 
			
		||||
  return toast.success("Account imported successfully!");
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										25
									
								
								state/actions/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
import { addFaucetAccount } from "./addFaucetAccount";
 | 
			
		||||
import { compileCode } from "./compileCode";
 | 
			
		||||
import { createNewFile } from "./createNewFile";
 | 
			
		||||
import { deployHook } from "./deployHook";
 | 
			
		||||
import { fetchFiles } from "./fetchFiles";
 | 
			
		||||
import { importAccount } from "./importAccount";
 | 
			
		||||
import { saveFile } from "./saveFile";
 | 
			
		||||
import { syncToGist } from "./syncToGist";
 | 
			
		||||
import { updateEditorSettings } from "./updateEditorSettings";
 | 
			
		||||
import { downloadAsZip } from "./downloadAsZip";
 | 
			
		||||
import { sendTransaction } from "./sendTransaction";
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  addFaucetAccount,
 | 
			
		||||
  compileCode,
 | 
			
		||||
  createNewFile,
 | 
			
		||||
  deployHook,
 | 
			
		||||
  fetchFiles,
 | 
			
		||||
  importAccount,
 | 
			
		||||
  saveFile,
 | 
			
		||||
  syncToGist,
 | 
			
		||||
  updateEditorSettings,
 | 
			
		||||
  downloadAsZip,
 | 
			
		||||
  sendTransaction
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										15
									
								
								state/actions/persistSplits.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
import { snapshot } from "valtio"
 | 
			
		||||
import state from ".."
 | 
			
		||||
 | 
			
		||||
export type SplitSize = number[]
 | 
			
		||||
 | 
			
		||||
export const saveSplit = (splitId: string, event: SplitSize) => {
 | 
			
		||||
  state.splits[splitId] = event
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getSplit = (splitId: string): SplitSize | null => {
 | 
			
		||||
  const { splits } = snapshot(state)
 | 
			
		||||
  const split = splits[splitId]
 | 
			
		||||
  return split ? split : null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								state/actions/saveFile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import state from '../index';
 | 
			
		||||
 | 
			
		||||
// Saves the current editor content to global state
 | 
			
		||||
export const saveFile = (showToast: boolean = true) => {
 | 
			
		||||
  const editorModels = state.editorCtx?.getModels();
 | 
			
		||||
  const sought = '/' + state.files[state.active].name;
 | 
			
		||||
  const currentModel = editorModels?.find((editorModel) => {
 | 
			
		||||
    return editorModel.uri.path.endsWith(sought);
 | 
			
		||||
  });
 | 
			
		||||
  if (state.files.length > 0) {
 | 
			
		||||
    state.files[state.active].content = currentModel?.getValue() || "";
 | 
			
		||||
  }
 | 
			
		||||
  if (showToast) {
 | 
			
		||||
    toast.success("Saved successfully", { position: "bottom-center" });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										53
									
								
								state/actions/sendTransaction.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
			
		||||
import { derive, sign } from "xrpl-accountlib";
 | 
			
		||||
 | 
			
		||||
import state from '..'
 | 
			
		||||
import type { IAccount } from "..";
 | 
			
		||||
 | 
			
		||||
interface TransactionOptions {
 | 
			
		||||
    TransactionType: string,
 | 
			
		||||
    Account?: string,
 | 
			
		||||
    Fee?: string,
 | 
			
		||||
    Destination?: string
 | 
			
		||||
    [index: string]: any
 | 
			
		||||
}
 | 
			
		||||
interface OtherOptions {
 | 
			
		||||
    logPrefix?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const sendTransaction = async (account: IAccount, txOptions: TransactionOptions, options?: OtherOptions) => {
 | 
			
		||||
    if (!state.client) throw Error('XRPL client not initalized')
 | 
			
		||||
 | 
			
		||||
    const { Fee = "1000", ...opts } = txOptions
 | 
			
		||||
    const tx: TransactionOptions = {
 | 
			
		||||
        Account: account.address,
 | 
			
		||||
        Sequence: account.sequence, // TODO auto-fillable
 | 
			
		||||
        Fee,  // TODO auto-fillable
 | 
			
		||||
        ...opts
 | 
			
		||||
    };
 | 
			
		||||
    const { logPrefix = '' } = options || {}
 | 
			
		||||
    try {
 | 
			
		||||
        const signedAccount = derive.familySeed(account.secret);
 | 
			
		||||
        const { signedTransaction } = sign(tx, signedAccount);
 | 
			
		||||
        const response = await state.client.send({
 | 
			
		||||
            command: "submit",
 | 
			
		||||
            tx_blob: signedTransaction,
 | 
			
		||||
        });
 | 
			
		||||
        if (response.engine_result === "tesSUCCESS") {
 | 
			
		||||
            state.transactionLogs.push({
 | 
			
		||||
                type: 'success',
 | 
			
		||||
                message: `${logPrefix}[${response.engine_result}] ${response.engine_result_message}`
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            state.transactionLogs.push({
 | 
			
		||||
                type: "error",
 | 
			
		||||
                message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        console.error(err);
 | 
			
		||||
        state.transactionLogs.push({
 | 
			
		||||
            type: "error",
 | 
			
		||||
            message: err instanceof Error ? `${logPrefix}Error: ${err.message}` : `${logPrefix}Something went wrong, try again later`,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										102
									
								
								state/actions/syncToGist.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,102 @@
 | 
			
		||||
import type { Session } from "next-auth";
 | 
			
		||||
import toast from "react-hot-toast";
 | 
			
		||||
import { Octokit } from "@octokit/core";
 | 
			
		||||
import Router from "next/router";
 | 
			
		||||
 | 
			
		||||
import state from '../index';
 | 
			
		||||
 | 
			
		||||
const octokit = new Octokit();
 | 
			
		||||
 | 
			
		||||
// Syncs the current files from the state to GitHub Gists.
 | 
			
		||||
export const syncToGist = async (
 | 
			
		||||
  session?: Session | null,
 | 
			
		||||
  createNewGist?: boolean
 | 
			
		||||
) => {
 | 
			
		||||
  let files: Record<string, { filename: string; content: string }> = {};
 | 
			
		||||
  state.gistLoading = true;
 | 
			
		||||
 | 
			
		||||
  if (!session || !session.user) {
 | 
			
		||||
    state.gistLoading = false;
 | 
			
		||||
    return toast.error("You need to be logged in!");
 | 
			
		||||
  }
 | 
			
		||||
  const toastId = toast.loading("Pushing to Gist");
 | 
			
		||||
  if (!state.files || !state.files.length) {
 | 
			
		||||
    state.gistLoading = false;
 | 
			
		||||
    return toast.error(`You need to create some files we can push to gist`, {
 | 
			
		||||
      id: toastId,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  if (
 | 
			
		||||
    state.gistId &&
 | 
			
		||||
    session?.user.username === state.gistOwner &&
 | 
			
		||||
    !createNewGist
 | 
			
		||||
  ) {
 | 
			
		||||
    // You can only remove files from Gist by updating file with empty contents
 | 
			
		||||
    // So we need to fetch existing files and compare those to local state
 | 
			
		||||
    // and then send empty content if we don't have matching files anymore
 | 
			
		||||
    // on local state
 | 
			
		||||
    const currentFilesRes = await octokit.request("GET /gists/{gist_id}", {
 | 
			
		||||
      gist_id: state.gistId,
 | 
			
		||||
    });
 | 
			
		||||
    if (currentFilesRes.data.files) {
 | 
			
		||||
      Object.keys(currentFilesRes?.data?.files).forEach((filename) => {
 | 
			
		||||
        files[`${filename}`] = { filename, content: "" };
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    state.files.forEach((file) => {
 | 
			
		||||
      files[`${file.name}`] = { filename: file.name, content: file.content };
 | 
			
		||||
    });
 | 
			
		||||
    // Update existing Gist
 | 
			
		||||
    octokit
 | 
			
		||||
      .request("PATCH /gists/{gist_id}", {
 | 
			
		||||
        gist_id: state.gistId,
 | 
			
		||||
        files,
 | 
			
		||||
        headers: {
 | 
			
		||||
          authorization: `token ${session?.accessToken || ""}`,
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
      .then((res) => {
 | 
			
		||||
        state.gistLoading = false;
 | 
			
		||||
        return toast.success("Updated to gist successfully!", { id: toastId });
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        console.log(err);
 | 
			
		||||
        state.gistLoading = false;
 | 
			
		||||
        return toast.error(`Could not update Gist, try again later!`, {
 | 
			
		||||
          id: toastId,
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
  } else {
 | 
			
		||||
    // Not Gist of the current user or it isn't Gist yet
 | 
			
		||||
    state.files.forEach((file) => {
 | 
			
		||||
      files[`${file.name}`] = { filename: file.name, content: file.content };
 | 
			
		||||
    });
 | 
			
		||||
    octokit
 | 
			
		||||
      .request("POST /gists", {
 | 
			
		||||
        files,
 | 
			
		||||
        public: true,
 | 
			
		||||
        headers: {
 | 
			
		||||
          authorization: `token ${session?.accessToken || ""}`,
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
      .then((res) => {
 | 
			
		||||
        state.gistLoading = false;
 | 
			
		||||
        state.gistOwner = res.data.owner?.login;
 | 
			
		||||
        state.gistId = res.data.id;
 | 
			
		||||
        state.gistName = Array.isArray(res.data.files)
 | 
			
		||||
          ? Object.keys(res.data?.files)?.[0]
 | 
			
		||||
          : "Untitled";
 | 
			
		||||
        Router.push({ pathname: `/develop/${res.data.id}` });
 | 
			
		||||
        return toast.success("Created new gist successfully!", { id: toastId });
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        console.log(err);
 | 
			
		||||
        state.gistLoading = false;
 | 
			
		||||
        return toast.error(`Could not create Gist, try again later!`, {
 | 
			
		||||
          id: toastId,
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default syncToGist;
 | 
			
		||||
							
								
								
									
										14
									
								
								state/actions/updateEditorSettings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
			
		||||
import state, { IState } from '../index';
 | 
			
		||||
 | 
			
		||||
// Updates editor settings and stores them
 | 
			
		||||
// in global state
 | 
			
		||||
export const updateEditorSettings = (
 | 
			
		||||
  editorSettings: IState["editorSettings"]
 | 
			
		||||
) => {
 | 
			
		||||
  state.editorCtx?.getModels().forEach((model) => {
 | 
			
		||||
    model.updateOptions({
 | 
			
		||||
      ...editorSettings,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  return (state.editorSettings = editorSettings);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										1
									
								
								state/constants/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
export * from './templates'
 | 
			
		||||
							
								
								
									
										10
									
								
								state/constants/templates.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
			
		||||
export const templateFileIds = {
 | 
			
		||||
    'starter': '1d14e51e2e02dc0a508cb0733767a914', // TODO currently same as accept
 | 
			
		||||
    'firewall': 'bcd6d0c0fcbe52545ddb802481ff9d26',
 | 
			
		||||
    'notary': 'a789c75f591eeab7932fd702ed8cf9ea',
 | 
			
		||||
    'carbon': '43925143fa19735d8c6505c34d3a6a47',
 | 
			
		||||
    'peggy': 'ceaf352e2a65741341033ab7ef05c448',
 | 
			
		||||
    'headers': '9b448e8a55fab11ef5d1274cb59f9cf3'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'hookmacro.h']
 | 
			
		||||
							
								
								
									
										150
									
								
								state/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,150 @@
 | 
			
		||||
import type monaco from "monaco-editor";
 | 
			
		||||
import { proxy, ref, subscribe } from "valtio";
 | 
			
		||||
import { devtools } from 'valtio/utils';
 | 
			
		||||
import { XrplClient } from "xrpl-client";
 | 
			
		||||
import { SplitSize } from "./actions/persistSplits";
 | 
			
		||||
 | 
			
		||||
export interface IFile {
 | 
			
		||||
  name: string;
 | 
			
		||||
  language: string;
 | 
			
		||||
  content: string;
 | 
			
		||||
  compiledContent?: ArrayBuffer | null;
 | 
			
		||||
  compiledWatContent?: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface FaucetAccountRes {
 | 
			
		||||
  address: string;
 | 
			
		||||
  secret: string;
 | 
			
		||||
  xrp: number;
 | 
			
		||||
  hash: string;
 | 
			
		||||
  code: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IAccount {
 | 
			
		||||
  name: string;
 | 
			
		||||
  address: string;
 | 
			
		||||
  secret: string;
 | 
			
		||||
  xrp: string;
 | 
			
		||||
  sequence: number;
 | 
			
		||||
  hooks: string[];
 | 
			
		||||
  isLoading: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ILog {
 | 
			
		||||
  type: "error" | "warning" | "log" | "success";
 | 
			
		||||
  message: string;
 | 
			
		||||
  link?: string;
 | 
			
		||||
  linkText?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IState {
 | 
			
		||||
  files: IFile[];
 | 
			
		||||
  gistId?: string | null;
 | 
			
		||||
  gistOwner?: string | null;
 | 
			
		||||
  gistName?: string | null;
 | 
			
		||||
  active: number;
 | 
			
		||||
  activeWat: number;
 | 
			
		||||
  loading: boolean;
 | 
			
		||||
  gistLoading: boolean;
 | 
			
		||||
  zipLoading: boolean;
 | 
			
		||||
  compiling: boolean;
 | 
			
		||||
  logs: ILog[];
 | 
			
		||||
  deployLogs: ILog[];
 | 
			
		||||
  transactionLogs: ILog[];
 | 
			
		||||
  debugLogs: ILog[];
 | 
			
		||||
  editorCtx?: typeof monaco.editor;
 | 
			
		||||
  editorSettings: {
 | 
			
		||||
    tabSize: number;
 | 
			
		||||
  };
 | 
			
		||||
  splits: {
 | 
			
		||||
    [id: string]: SplitSize
 | 
			
		||||
  };
 | 
			
		||||
  client: XrplClient | null;
 | 
			
		||||
  clientStatus: "offline" | "online";
 | 
			
		||||
  mainModalOpen: boolean;
 | 
			
		||||
  accounts: IAccount[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// let localStorageState: null | string = null;
 | 
			
		||||
let initialState: IState = {
 | 
			
		||||
  files: [],
 | 
			
		||||
  // active file index on the Develop page editor
 | 
			
		||||
  active: 0,
 | 
			
		||||
  // Active file index on the Deploy page editor
 | 
			
		||||
  activeWat: 0,
 | 
			
		||||
  loading: false,
 | 
			
		||||
  compiling: false,
 | 
			
		||||
  logs: [],
 | 
			
		||||
  deployLogs: [],
 | 
			
		||||
  transactionLogs: [],
 | 
			
		||||
  debugLogs: [],
 | 
			
		||||
  editorCtx: undefined,
 | 
			
		||||
  gistId: undefined,
 | 
			
		||||
  gistOwner: undefined,
 | 
			
		||||
  gistName: undefined,
 | 
			
		||||
  gistLoading: false,
 | 
			
		||||
  zipLoading: false,
 | 
			
		||||
  editorSettings: {
 | 
			
		||||
    tabSize: 2,
 | 
			
		||||
  },
 | 
			
		||||
  splits: {},
 | 
			
		||||
  client: null,
 | 
			
		||||
  clientStatus: "offline" as "offline",
 | 
			
		||||
  mainModalOpen: false,
 | 
			
		||||
  accounts: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let localStorageAccounts: string | null = null;
 | 
			
		||||
let initialAccounts: IAccount[] = [];
 | 
			
		||||
 | 
			
		||||
// TODO: What exactly should we store in localStorage? editorSettings, splits, accounts?
 | 
			
		||||
 | 
			
		||||
// Check if there's a persited accounts in localStorage
 | 
			
		||||
if (typeof window !== "undefined") {
 | 
			
		||||
  try {
 | 
			
		||||
    localStorageAccounts = localStorage.getItem("hooksIdeAccounts");
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.log(`localStorage state broken`);
 | 
			
		||||
    localStorage.removeItem("hooksIdeAccounts");
 | 
			
		||||
  }
 | 
			
		||||
  if (localStorageAccounts) {
 | 
			
		||||
    initialAccounts = JSON.parse(localStorageAccounts);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Initialize state
 | 
			
		||||
const state = proxy<IState>({
 | 
			
		||||
  ...initialState,
 | 
			
		||||
  accounts: initialAccounts.length > 0 ? initialAccounts : [],
 | 
			
		||||
  logs: [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Initialize socket connection
 | 
			
		||||
const client = new XrplClient("wss://hooks-testnet.xrpl-labs.com");
 | 
			
		||||
 | 
			
		||||
client.on("online", () => {
 | 
			
		||||
  state.client = ref(client);
 | 
			
		||||
  state.clientStatus = "online";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
client.on("offline", () => {
 | 
			
		||||
  state.clientStatus = "offline";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (process.env.NODE_ENV !== "production") {
 | 
			
		||||
  devtools(state, "Files State");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (typeof window !== "undefined") {
 | 
			
		||||
  subscribe(state, () => {
 | 
			
		||||
    const { accounts, active } = state;
 | 
			
		||||
    const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
 | 
			
		||||
    localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
 | 
			
		||||
    if (!state.files[active]?.compiledWatContent) {
 | 
			
		||||
      state.activeWat = 0;
 | 
			
		||||
    } else {
 | 
			
		||||
      state.activeWat = active;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
export default state
 | 
			
		||||
@@ -1,24 +1,25 @@
 | 
			
		||||
// stitches.config.ts
 | 
			
		||||
import type Stitches from '@stitches/react';
 | 
			
		||||
import { createStitches } from '@stitches/react';
 | 
			
		||||
import type Stitches from "@stitches/react";
 | 
			
		||||
import { createStitches } from "@stitches/react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  gray,
 | 
			
		||||
  blue,
 | 
			
		||||
  red,
 | 
			
		||||
  green,
 | 
			
		||||
  plum,
 | 
			
		||||
  crimson,
 | 
			
		||||
  grass,
 | 
			
		||||
  slate,
 | 
			
		||||
  pink,
 | 
			
		||||
  yellow,
 | 
			
		||||
  mauve,
 | 
			
		||||
  amber,
 | 
			
		||||
  purple,
 | 
			
		||||
  grayDark,
 | 
			
		||||
  blueDark,
 | 
			
		||||
  redDark,
 | 
			
		||||
  greenDark,
 | 
			
		||||
  plumDark,
 | 
			
		||||
  crimsonDark,
 | 
			
		||||
  grassDark,
 | 
			
		||||
  slateDark,
 | 
			
		||||
  pinkDark,
 | 
			
		||||
  yellowDark
 | 
			
		||||
} from '@radix-ui/colors';
 | 
			
		||||
  mauveDark,
 | 
			
		||||
  amberDark,
 | 
			
		||||
  purpleDark,
 | 
			
		||||
} from "@radix-ui/colors";
 | 
			
		||||
 | 
			
		||||
export const {
 | 
			
		||||
  styled,
 | 
			
		||||
@@ -34,25 +35,28 @@ export const {
 | 
			
		||||
    colors: {
 | 
			
		||||
      ...gray,
 | 
			
		||||
      ...blue,
 | 
			
		||||
      ...red,
 | 
			
		||||
      ...green,
 | 
			
		||||
      ...plum,
 | 
			
		||||
      ...crimson,
 | 
			
		||||
      ...grass,
 | 
			
		||||
      ...slate,
 | 
			
		||||
      ...pink,
 | 
			
		||||
      ...yellow,
 | 
			
		||||
      ...mauve,
 | 
			
		||||
      ...amber,
 | 
			
		||||
      ...purple,
 | 
			
		||||
      accent: "#9D2DFF",
 | 
			
		||||
      background: "$gray1",
 | 
			
		||||
      backgroundAlt: "$gray4",
 | 
			
		||||
      text: "$gray12",
 | 
			
		||||
      primary: "$plum",
 | 
			
		||||
      white: "white",
 | 
			
		||||
      black: "black"
 | 
			
		||||
      black: "black",
 | 
			
		||||
      deep: "rgb(244, 244, 244)",
 | 
			
		||||
    },
 | 
			
		||||
    fonts: {
 | 
			
		||||
      body: 'Work Sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
 | 
			
		||||
      heading: 'Work Sans, sans-serif',
 | 
			
		||||
      monospace: 'Roboto, monospace',
 | 
			
		||||
      heading: "Work Sans, sans-serif",
 | 
			
		||||
      monospace: "Roboto Mono, monospace",
 | 
			
		||||
    },
 | 
			
		||||
    fontSizes: {
 | 
			
		||||
      xs: "0.75rem",
 | 
			
		||||
      xs: "0.6875rem",
 | 
			
		||||
      sm: "0.875rem",
 | 
			
		||||
      md: "1rem",
 | 
			
		||||
      lg: "1.125rem",
 | 
			
		||||
@@ -65,7 +69,7 @@ export const {
 | 
			
		||||
      "7xl": "4.5rem",
 | 
			
		||||
      "8xl": "6rem",
 | 
			
		||||
      "9xl": "8rem",
 | 
			
		||||
      default: '$md'
 | 
			
		||||
      default: "$md",
 | 
			
		||||
    },
 | 
			
		||||
    space: {
 | 
			
		||||
      px: "1px",
 | 
			
		||||
@@ -101,15 +105,15 @@ export const {
 | 
			
		||||
      72: "18rem",
 | 
			
		||||
      80: "20rem",
 | 
			
		||||
      96: "24rem",
 | 
			
		||||
      "widePlus": '2048px',
 | 
			
		||||
      "wide": '1536px',
 | 
			
		||||
      "layoutPlus": '1260px',
 | 
			
		||||
      "layout": '1024px',
 | 
			
		||||
      "copyUltra": '980px',
 | 
			
		||||
      "copyPlus": '768px',
 | 
			
		||||
      "copy": '680px',
 | 
			
		||||
      "narrowPlus": '600px',
 | 
			
		||||
      "narrow": '512px',
 | 
			
		||||
      widePlus: "2048px",
 | 
			
		||||
      wide: "1536px",
 | 
			
		||||
      layoutPlus: "1260px",
 | 
			
		||||
      layout: "1024px",
 | 
			
		||||
      copyUltra: "980px",
 | 
			
		||||
      copyPlus: "768px",
 | 
			
		||||
      copy: "680px",
 | 
			
		||||
      narrowPlus: "600px",
 | 
			
		||||
      narrow: "512px",
 | 
			
		||||
      xs: "20rem",
 | 
			
		||||
      sm: "24rem",
 | 
			
		||||
      md: "28rem",
 | 
			
		||||
@@ -188,7 +192,7 @@ export const {
 | 
			
		||||
    },
 | 
			
		||||
    fontWeights: {
 | 
			
		||||
      body: 400,
 | 
			
		||||
      heading: 400,
 | 
			
		||||
      heading: 700,
 | 
			
		||||
      bold: 700,
 | 
			
		||||
    },
 | 
			
		||||
    lineHeights: {
 | 
			
		||||
@@ -209,62 +213,112 @@ export const {
 | 
			
		||||
    lg: "(min-width: 62em)",
 | 
			
		||||
    xl: "(min-width: 80em)",
 | 
			
		||||
    "2xl": "(min-width: 96em)",
 | 
			
		||||
    hover: '(any-hover: hover)',
 | 
			
		||||
    dark: '(prefers-color-scheme: dark)',
 | 
			
		||||
    light: '(prefers-color-scheme: light)',
 | 
			
		||||
    hover: "(any-hover: hover)",
 | 
			
		||||
    dark: "(prefers-color-scheme: dark)",
 | 
			
		||||
    light: "(prefers-color-scheme: light)",
 | 
			
		||||
  },
 | 
			
		||||
  utils: {
 | 
			
		||||
    // Abbreviated margin properties
 | 
			
		||||
    m: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'margin'>) => ({
 | 
			
		||||
    m: (
 | 
			
		||||
      value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"margin">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      margin: value,
 | 
			
		||||
    }),
 | 
			
		||||
    mt: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginTop'>) => ({
 | 
			
		||||
    mt: (
 | 
			
		||||
      value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"marginTop">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      marginTop: value,
 | 
			
		||||
    }),
 | 
			
		||||
    mr: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginRight'>) => ({
 | 
			
		||||
    mr: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"marginRight">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      marginRight: value,
 | 
			
		||||
    }),
 | 
			
		||||
    mb: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginBottom'>) => ({
 | 
			
		||||
    mb: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"marginBottom">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      marginBottom: value,
 | 
			
		||||
    }),
 | 
			
		||||
    ml: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginLeft'>) => ({
 | 
			
		||||
    ml: (
 | 
			
		||||
      value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"marginLeft">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      marginLeft: value,
 | 
			
		||||
    }),
 | 
			
		||||
    mx: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginLeft' | 'marginRight'>) => ({
 | 
			
		||||
    mx: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"marginLeft" | "marginRight">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      marginLeft: value,
 | 
			
		||||
      marginRight: value,
 | 
			
		||||
    }),
 | 
			
		||||
    my: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginTop' | 'marginBottom'>) => ({
 | 
			
		||||
    my: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"marginTop" | "marginBottom">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      marginTop: value,
 | 
			
		||||
      marginBottom: value,
 | 
			
		||||
    }),
 | 
			
		||||
    // Abbreviated margin properties
 | 
			
		||||
    p: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'padding'>) => ({
 | 
			
		||||
    p: (
 | 
			
		||||
      value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"padding">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      padding: value,
 | 
			
		||||
    }),
 | 
			
		||||
    pt: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingTop'>) => ({
 | 
			
		||||
    pt: (
 | 
			
		||||
      value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"paddingTop">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      paddingTop: value,
 | 
			
		||||
    }),
 | 
			
		||||
    pr: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingRight'>) => ({
 | 
			
		||||
    pr: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"paddingRight">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      paddingRight: value,
 | 
			
		||||
    }),
 | 
			
		||||
    pb: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingBottom'>) => ({
 | 
			
		||||
    pb: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"paddingBottom">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      paddingBottom: value,
 | 
			
		||||
    }),
 | 
			
		||||
    pl: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingLeft'>) => ({
 | 
			
		||||
    pl: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"paddingLeft">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      paddingLeft: value,
 | 
			
		||||
    }),
 | 
			
		||||
    px: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingLeft' | 'paddingRight'>) => ({
 | 
			
		||||
    px: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"paddingLeft" | "paddingRight">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      paddingLeft: value,
 | 
			
		||||
      paddingRight: value,
 | 
			
		||||
    }),
 | 
			
		||||
    py: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingTop' | 'paddingBottom'>) => ({
 | 
			
		||||
    py: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"paddingTop" | "paddingBottom">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      paddingTop: value,
 | 
			
		||||
      paddingBottom: value,
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    // A property for applying width/height together
 | 
			
		||||
    size: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'width' | 'height'>) => ({
 | 
			
		||||
    size: (
 | 
			
		||||
      value:
 | 
			
		||||
        | Stitches.ScaleValue<"space">
 | 
			
		||||
        | Stitches.PropertyValue<"width" | "height">
 | 
			
		||||
    ) => ({
 | 
			
		||||
      width: value,
 | 
			
		||||
      height: value,
 | 
			
		||||
    }),
 | 
			
		||||
@@ -273,32 +327,44 @@ export const {
 | 
			
		||||
    // }),
 | 
			
		||||
 | 
			
		||||
    // A property to apply linear gradient
 | 
			
		||||
    linearGradient: (value: Stitches.ScaleValue<'space'>) => ({
 | 
			
		||||
    linearGradient: (value: Stitches.ScaleValue<"space">) => ({
 | 
			
		||||
      backgroundImage: `linear-gradient(${value})`,
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    // An abbreviated property for border-radius
 | 
			
		||||
    br: (value: Stitches.ScaleValue<'space'>) => ({
 | 
			
		||||
    br: (value: Stitches.ScaleValue<"space">) => ({
 | 
			
		||||
      borderRadius: value,
 | 
			
		||||
    }),
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const darkTheme = createTheme('dark', {
 | 
			
		||||
export const darkTheme = createTheme("dark", {
 | 
			
		||||
  colors: {
 | 
			
		||||
    ...grayDark,
 | 
			
		||||
    ...blueDark,
 | 
			
		||||
    ...redDark,
 | 
			
		||||
    ...greenDark,
 | 
			
		||||
    ...plumDark,
 | 
			
		||||
    ...crimsonDark,
 | 
			
		||||
    ...grassDark,
 | 
			
		||||
    ...slateDark,
 | 
			
		||||
    ...pinkDark,
 | 
			
		||||
    ...yellowDark
 | 
			
		||||
    ...mauveDark,
 | 
			
		||||
    ...amberDark,
 | 
			
		||||
    ...purpleDark,
 | 
			
		||||
    deep: "rgb(10, 10, 10)",
 | 
			
		||||
    // backgroundA: transparentize(0.1, grayDark.gray1),
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const globalStyles = globalCss({
 | 
			
		||||
  // body: { backgroundColor: '$background', color: '$text', fontFamily: 'Helvetica' },
 | 
			
		||||
  'html, body': { backgroundColor: '$gray1', color: '$gray12', fontFamily: '$body', fontSize: '$md' },
 | 
			
		||||
  "html, body": {
 | 
			
		||||
    backgroundColor: "$mauve2",
 | 
			
		||||
    color: "$mauve12",
 | 
			
		||||
    fontFamily: "$body",
 | 
			
		||||
    fontSize: "$md",
 | 
			
		||||
    "-webkit-font-smoothing": "antialiased",
 | 
			
		||||
    "-moz-osx-font-smoothing": "grayscale",
 | 
			
		||||
  },
 | 
			
		||||
  a: {
 | 
			
		||||
    color: "inherit",
 | 
			
		||||
    textDecoration: "none",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -6,13 +6,47 @@ body,
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a {
 | 
			
		||||
  color: inherit;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  overflow-y: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
* {
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gutter {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  transition: border-color 0.3s, background-color 0.3s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gutter-vertical {
 | 
			
		||||
  margin-top: -4px;
 | 
			
		||||
}
 | 
			
		||||
.gutter-horizontal {
 | 
			
		||||
  margin-left: -4px;
 | 
			
		||||
}
 | 
			
		||||
.gutter-vertical:hover {
 | 
			
		||||
  cursor: row-resize;
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.25);
 | 
			
		||||
}
 | 
			
		||||
html.light .gutter-vertical:hover {
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.25);
 | 
			
		||||
}
 | 
			
		||||
.gutter-horizontal:hover {
 | 
			
		||||
  cursor: col-resize;
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.25);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html.light .gutter-horizontal:hover {
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.25);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Adjust Monaco tooltip stylings */
 | 
			
		||||
.markdown-hover h3 {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
.monaco-editor .monaco-hover hr {
 | 
			
		||||
  margin: 8px 0;
 | 
			
		||||
}
 | 
			
		||||
.monaco-editor .monaco-hover {
 | 
			
		||||
  z-index: 9999;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
  "inherit": true,
 | 
			
		||||
  "rules": [
 | 
			
		||||
    {
 | 
			
		||||
      "background": "1a1d1e",
 | 
			
		||||
      "background": "161618",
 | 
			
		||||
      "token": ""
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
@@ -182,10 +182,10 @@
 | 
			
		||||
  ],
 | 
			
		||||
  "colors": {
 | 
			
		||||
    "editor.foreground": "#D0D0FF",
 | 
			
		||||
    "editor.background": "#202425",
 | 
			
		||||
    "editor.background": "#1C1C1F",
 | 
			
		||||
    "editor.selectionBackground": "#ffffff30",
 | 
			
		||||
    "editor.lineHighlightBackground": "#ffffff20",
 | 
			
		||||
    "editorCursor.foreground": "#7070FF",
 | 
			
		||||
    "editorWhitespace.foreground": "#BFBFBF"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
  "inherit": true,
 | 
			
		||||
  "rules": [
 | 
			
		||||
    {
 | 
			
		||||
      "background": "FFFFFF",
 | 
			
		||||
      "background": "F4F2F4",
 | 
			
		||||
      "token": ""
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
@@ -89,10 +89,10 @@
 | 
			
		||||
  ],
 | 
			
		||||
  "colors": {
 | 
			
		||||
    "editor.foreground": "#000000",
 | 
			
		||||
    "editor.background": "#f1f3f5",
 | 
			
		||||
    "editor.background": "#F9F8F9",
 | 
			
		||||
    "editor.selectionBackground": "#B5D5FF",
 | 
			
		||||
    "editor.lineHighlightBackground": "#00000012",
 | 
			
		||||
    "editorCursor.foreground": "#000000",
 | 
			
		||||
    "editorWhitespace.foreground": "#BFBFBF"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,8 @@
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "jsx": "preserve",
 | 
			
		||||
    "incremental": true
 | 
			
		||||
    "incremental": true,
 | 
			
		||||
    "noUnusedLocals": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "next-env.d.ts",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								utils/decodeBinary.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
import { decodeRestrictedBase64ToBytes } from "./decodeRestrictedBase64ToBytes";
 | 
			
		||||
import { isZlibData, decompressZlib } from "./zlib";
 | 
			
		||||
import { fromByteArray } from "base64-js";
 | 
			
		||||
 | 
			
		||||
export async function decodeBinary(input: string): Promise<ArrayBuffer> {
 | 
			
		||||
  let data = decodeRestrictedBase64ToBytes(input);
 | 
			
		||||
  if (isZlibData(data)) {
 | 
			
		||||
    data = await decompressZlib(data);
 | 
			
		||||
  }
 | 
			
		||||
  return data.buffer as ArrayBuffer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function encodeBinary(input: ArrayBuffer): string {
 | 
			
		||||
  return fromByteArray(new Uint8Array(input));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								utils/decodeRestrictedBase64ToBytes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,44 @@
 | 
			
		||||
const base64DecodeMap = [ // starts at 0x2B
 | 
			
		||||
  62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
 | 
			
		||||
  0, 0, 0, 0, 0, 0, 0, // 0x3A-0x40
 | 
			
		||||
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
 | 
			
		||||
  19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, // 0x5B-0x0x60
 | 
			
		||||
  26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
 | 
			
		||||
  44, 45, 46, 47, 48, 49, 50, 51
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const base64DecodeMapOffset = 0x2B;
 | 
			
		||||
const base64EOF = 0x3D;
 | 
			
		||||
 | 
			
		||||
export function decodeRestrictedBase64ToBytes(encoded: string) {
 | 
			
		||||
  let ch: any;
 | 
			
		||||
  let code: any;
 | 
			
		||||
  let code2: any;
 | 
			
		||||
 | 
			
		||||
  const len = encoded.length;
 | 
			
		||||
  const padding = encoded.charAt(len - 2) === "=" ? 2 : encoded.charAt(len - 1) === "=" ? 1 : 0;
 | 
			
		||||
  const decoded = new Uint8Array((encoded.length >> 2) * 3 - padding);
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, j = 0; i < encoded.length;) {
 | 
			
		||||
    ch = encoded.charCodeAt(i++);
 | 
			
		||||
    code = base64DecodeMap[ch - base64DecodeMapOffset];
 | 
			
		||||
    ch = encoded.charCodeAt(i++);
 | 
			
		||||
    code2 = base64DecodeMap[ch - base64DecodeMapOffset];
 | 
			
		||||
    decoded[j++] = (code << 2) | ((code2 & 0x30) >> 4);
 | 
			
		||||
 | 
			
		||||
    ch = encoded.charCodeAt(i++);
 | 
			
		||||
    if (ch === base64EOF) {
 | 
			
		||||
      return decoded;
 | 
			
		||||
    }
 | 
			
		||||
    code = base64DecodeMap[ch - base64DecodeMapOffset];
 | 
			
		||||
    decoded[j++] = ((code2 & 0x0f) << 4) | ((code & 0x3c) >> 2);
 | 
			
		||||
 | 
			
		||||
    ch = encoded.charCodeAt(i++);
 | 
			
		||||
    if (ch === base64EOF) {
 | 
			
		||||
      return decoded;
 | 
			
		||||
    }
 | 
			
		||||
    code2 = base64DecodeMap[ch - base64DecodeMapOffset];
 | 
			
		||||
    decoded[j++] = ((code & 0x03) << 6) | code2;
 | 
			
		||||
  }
 | 
			
		||||
  return decoded;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								utils/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
interface File {
 | 
			
		||||
    name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const guessZipFileName = (files: File[]) => {
 | 
			
		||||
    let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
 | 
			
		||||
    parts = parts.length > 1 ? parts.slice(0, -1) : parts
 | 
			
		||||
    return parts.join('')
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								utils/languageClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,50 @@
 | 
			
		||||
import { MessageConnection } from "@codingame/monaco-jsonrpc";
 | 
			
		||||
import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient";
 | 
			
		||||
import Router from "next/router";
 | 
			
		||||
import normalizeUrl from "normalize-url";
 | 
			
		||||
import ReconnectingWebSocket from "reconnecting-websocket";
 | 
			
		||||
 | 
			
		||||
export function createLanguageClient(connection: MessageConnection): MonacoLanguageClient {
 | 
			
		||||
  return new MonacoLanguageClient({
 | 
			
		||||
    name: "Clangd Language Client",
 | 
			
		||||
    clientOptions: {
 | 
			
		||||
      // use a language id as a document selector
 | 
			
		||||
      documentSelector: ['c', 'h'],
 | 
			
		||||
      // disable the default error handler
 | 
			
		||||
      errorHandler: {
 | 
			
		||||
        error: () => ErrorAction.Continue,
 | 
			
		||||
        closed: () => {
 | 
			
		||||
          if (Router.pathname.includes('/develop')) {
 | 
			
		||||
            return CloseAction.Restart
 | 
			
		||||
          } else {
 | 
			
		||||
            return CloseAction.DoNotRestart
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    // create a language client connection from the JSON RPC connection on demand
 | 
			
		||||
    connectionProvider: {
 | 
			
		||||
      get: (errorHandler, closeHandler) => {
 | 
			
		||||
        return Promise.resolve(createConnection(connection, errorHandler, closeHandler))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createUrl(path: string): string {
 | 
			
		||||
  const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
 | 
			
		||||
  return normalizeUrl(`${protocol}://${location.host}${location.pathname}${path}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createWebSocket(url: string) {
 | 
			
		||||
  const socketOptions = {
 | 
			
		||||
    maxReconnectionDelay: 10000,
 | 
			
		||||
    minReconnectionDelay: 1000,
 | 
			
		||||
    reconnectionDelayGrowFactor: 1.3,
 | 
			
		||||
    connectionTimeout: 10000,
 | 
			
		||||
    maxRetries: Infinity,
 | 
			
		||||
    debug: false
 | 
			
		||||
  };
 | 
			
		||||
  return new ReconnectingWebSocket(url, [], socketOptions);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								utils/libwabt.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										8
									
								
								utils/truncate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
			
		||||
const truncate = (str: string, max: number = 8) => {
 | 
			
		||||
  const array = str.trim().split('');
 | 
			
		||||
  const ellipsis = array.length > max ? '...' : '';
 | 
			
		||||
 | 
			
		||||
  return array.slice(0, max).join('') + ellipsis;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default truncate
 | 
			
		||||
							
								
								
									
										32
									
								
								utils/zip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
			
		||||
import JSZip, { JSZipFileOptions } from 'jszip'
 | 
			
		||||
import { saveAs } from 'file-saver'
 | 
			
		||||
 | 
			
		||||
interface File {
 | 
			
		||||
    name: string
 | 
			
		||||
    content: any
 | 
			
		||||
    options?: JSZipFileOptions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Zipped {
 | 
			
		||||
    saveFile: (filename: string) => void
 | 
			
		||||
    data: Blob
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const createZip = async (files: File[]): Promise<Zipped> => {
 | 
			
		||||
    const zip = new JSZip()
 | 
			
		||||
 | 
			
		||||
    files.forEach(({ name, content, options }) => {
 | 
			
		||||
        zip.file(name, content, options)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const data = await zip.generateAsync({ type: "blob" })
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        saveFile: (filename: string) =>
 | 
			
		||||
            saveAs(data, filename),
 | 
			
		||||
        data
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								utils/zlib.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
			
		||||
export function isZlibData(data: Uint8Array): boolean {
 | 
			
		||||
  // @ts-expect-error
 | 
			
		||||
  const [firstByte, secondByte] = data;
 | 
			
		||||
  return firstByte === 0x78 && (secondByte === 0x01 || secondByte === 0x9C || secondByte === 0xDA);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function decompressZlib(data: Uint8Array): Promise<Uint8Array> {
 | 
			
		||||
  const { inflate } = await import("pako");
 | 
			
		||||
  return inflate(data);
 | 
			
		||||
}
 | 
			
		||||