Compare commits
196 Commits
feat/impro
...
fix/nft-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3340857575 | ||
|
|
e1f34c4beb | ||
|
|
54a89c969e | ||
|
|
ded867d997 | ||
|
|
0fce9af77c | ||
|
|
55c68c580a | ||
|
|
832a7997d1 | ||
|
|
4528e5a16e | ||
|
|
38f064c6d8 | ||
|
|
fbf4565dbc | ||
|
|
9001c64fed | ||
|
|
03b768db4e | ||
|
|
825af0db89 | ||
|
|
31043f33ab | ||
|
|
39699a1cb9 | ||
|
|
b50b300307 | ||
|
|
82c06cbb12 | ||
|
|
423ee18e6a | ||
|
|
3bb26d0c9b | ||
|
|
43c83d0de6 | ||
|
|
6bb407cb0f | ||
|
|
d7b29ba809 | ||
|
|
ca81d8ad41 | ||
|
|
a7e59d7b73 | ||
|
|
8f6b28cef5 | ||
|
|
c1815f272b | ||
|
|
76871a8041 | ||
|
|
d2033c8035 | ||
|
|
48daf1c5c8 | ||
|
|
a3365e4beb | ||
|
|
45d813cdad | ||
|
|
911416aa2f | ||
|
|
2d836af9ed | ||
|
|
4f0fc838be | ||
|
|
fa93912c38 | ||
|
|
f5cb76c302 | ||
|
|
df1d65dcab | ||
|
|
1513f78991 | ||
|
|
3a064f307b | ||
|
|
6fca05f310 | ||
|
|
31e67d382f | ||
|
|
27475301e4 | ||
|
|
934283976a | ||
|
|
2d9ca2674e | ||
|
|
221c727af6 | ||
|
|
3b0a8c44c9 | ||
|
|
0d9e9e7b45 | ||
|
|
094f739b80 | ||
|
|
a2077f9592 | ||
|
|
59d9c5356c | ||
|
|
2a10d525fb | ||
|
|
2c2bf59bcd | ||
|
|
79bd5da3d2 | ||
|
|
1fab69ef9b | ||
|
|
6418094b0f | ||
|
|
2d82966b3b | ||
|
|
dbbbfdb2f0 | ||
|
|
9923dd9390 | ||
|
|
052be354d1 | ||
|
|
737523fe8d | ||
|
|
034fc3423b | ||
|
|
b23b0066bb | ||
|
|
380b6b1afd | ||
|
|
c7d77b26b5 | ||
|
|
aa62251780 | ||
|
|
4b2b4b25c0 | ||
|
|
6dd5712573 | ||
|
|
6611a94652 | ||
|
|
93c5ef231e | ||
|
|
53c2104b94 | ||
|
|
c336ff8334 | ||
|
|
2086291d4d | ||
|
|
5505f0ac87 | ||
|
|
2cf92b908d | ||
|
|
70de876f75 | ||
|
|
58cde29fff | ||
|
|
a06fb06610 | ||
|
|
1f5a9731bb | ||
|
|
ef4f95ca3e | ||
|
|
fb9814ec76 | ||
|
|
7f6b989f15 | ||
|
|
d459b2ee92 | ||
|
|
6ee1a09aaa | ||
|
|
dd2228fb35 | ||
|
|
ca52a5e064 | ||
|
|
df0f8abe62 | ||
|
|
a6c4db1951 | ||
|
|
1c91003164 | ||
|
|
66be0efbbd | ||
|
|
9ab64ec062 | ||
|
|
e77a5e234f | ||
|
|
d2f618512a | ||
|
|
f5063de2c9 | ||
|
|
1ee8dcb536 | ||
|
|
7f6f9c11db | ||
|
|
b2b7059774 | ||
|
|
41ba096ef9 | ||
|
|
8b72086c04 | ||
|
|
895b34cc68 | ||
|
|
b9da659f83 | ||
|
|
3897f2d823 | ||
|
|
6a3ff3e1d7 | ||
|
|
bf792f1495 | ||
|
|
df3210a663 | ||
|
|
bad7730c32 | ||
|
|
adb6a78549 | ||
|
|
8cc27f20c3 | ||
|
|
8e49a3f5f1 | ||
|
|
3179757469 | ||
|
|
554cfb3db9 | ||
|
|
637a066f69 | ||
|
|
c9a852e9be | ||
|
|
307a5407eb | ||
|
|
faa28845c8 | ||
|
|
168d11d48e | ||
|
|
60f2bb558c | ||
|
|
fdf33b9f45 | ||
|
|
d05180d148 | ||
|
|
bfaa6be17d | ||
|
|
9e368dec84 | ||
|
|
25eec6980f | ||
|
|
8e2f20c5ac | ||
|
|
a3d094e873 | ||
|
|
ef70bfb13a | ||
|
|
c26c7c13d1 | ||
|
|
fc461ddd0d | ||
|
|
c7001f6089 | ||
|
|
243cbfec08 | ||
|
|
1295e7fa41 | ||
|
|
793623d216 | ||
|
|
0cde0eb240 | ||
|
|
e2acb48e03 | ||
|
|
a4373bb970 | ||
|
|
cfb791092a | ||
|
|
3fcbac5ed9 | ||
|
|
c40b272ce8 | ||
|
|
860ff66a8a | ||
|
|
f4f700bea1 | ||
|
|
789bc00ac3 | ||
|
|
6a0aabdeda | ||
|
|
175b6266e8 | ||
|
|
621482e2ee | ||
|
|
e55f48bc83 | ||
|
|
3e9e26a46a | ||
|
|
f0e730bb9b | ||
|
|
6ce4828fc6 | ||
|
|
bb0a246ae5 | ||
|
|
3af2bad536 | ||
|
|
4f1b877db0 | ||
|
|
0289d64f5e | ||
|
|
868a0bcf78 | ||
|
|
aab2476a05 | ||
|
|
cb25986d72 | ||
|
|
309ad57173 | ||
|
|
53afb1d3d1 | ||
|
|
31ff7c0e28 | ||
|
|
dfa35df465 | ||
|
|
f163b052e1 | ||
|
|
25c5b9c015 | ||
|
|
407e3946ce | ||
|
|
dc5b0d71eb | ||
|
|
3fd6c3f50e | ||
|
|
ec8bfc5eee | ||
|
|
b4a0bcb90d | ||
|
|
2c729e2aa4 | ||
|
|
1cb2542170 | ||
|
|
00b309df34 | ||
|
|
a6fc730de6 | ||
|
|
2245c5a221 | ||
|
|
60c33661ad | ||
|
|
ea21c85038 | ||
|
|
5478f43609 | ||
|
|
a9b64abb85 | ||
|
|
c6ced424d8 | ||
|
|
3a1159cffc | ||
|
|
3136de1bd1 | ||
|
|
67ffd3f1b4 | ||
|
|
8508cb69c4 | ||
|
|
89217d2633 | ||
|
|
ba1b64391c | ||
|
|
098d919a77 | ||
|
|
b2af37ab4b | ||
|
|
dcb7e94e86 | ||
|
|
67848b3d8d | ||
|
|
31a86263a1 | ||
|
|
4d0025afc1 | ||
|
|
f85bd2398d | ||
|
|
a2a6596cc5 | ||
|
|
37208ce97e | ||
|
|
bf4042926d | ||
|
|
3ccc1c16ac | ||
|
|
135f0c91a1 | ||
|
|
8f5786e242 | ||
|
|
810eb4ca27 | ||
|
|
e6574f9f12 | ||
|
|
1a6726fabf |
@@ -5,7 +5,8 @@ GITHUB_ID=""
|
|||||||
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
|
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
|
||||||
NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000"
|
NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000"
|
||||||
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
|
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
|
||||||
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v2.xrpl-labs.com"
|
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v3.xrpl-labs.com"
|
||||||
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v2-debugstream.xrpl-labs.com"
|
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v3-debugstream.xrpl-labs.com"
|
||||||
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v2-explorer.xrpl-labs.com"
|
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v3-explorer.xrpl-labs.com"
|
||||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
NEXT_PUBLIC_NETWORK_ID="21338"
|
||||||
|
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -32,3 +32,8 @@ yarn-error.log*
|
|||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
.yarnrc.yml
|
||||||
|
.yarn/
|
||||||
|
|||||||
@@ -1 +1,38 @@
|
|||||||
*.md
|
See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
*.md
|
||||||
|
utils/libwabt.js
|
||||||
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"semi": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
@@ -1,93 +1,94 @@
|
|||||||
import toast from "react-hot-toast";
|
import toast from 'react-hot-toast'
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from 'valtio'
|
||||||
import { ArrowSquareOut, Copy, Trash, Wallet, X } from "phosphor-react";
|
import { ArrowSquareOut, Copy, Trash, Wallet, X } from 'phosphor-react'
|
||||||
import React, { useEffect, useState, FC } from "react";
|
import React, { useEffect, useState, FC } from 'react'
|
||||||
import Dinero from "dinero.js";
|
import Dinero from 'dinero.js'
|
||||||
|
|
||||||
import Button from "./Button";
|
import Button from './Button'
|
||||||
import { addFaucetAccount, importAccount } from "../state/actions";
|
import { addFaucetAccount, importAccount } from '../state/actions'
|
||||||
import state from "../state";
|
import state from '../state'
|
||||||
import Box from "./Box";
|
import Box from './Box'
|
||||||
import { Container, Heading, Stack, Text, Flex } from ".";
|
import { Container, Heading, Stack, Text, Flex } from '.'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogTrigger,
|
DialogTrigger
|
||||||
} from "./Dialog";
|
} from './Dialog'
|
||||||
import { css } from "../stitches.config";
|
import { css } from '../stitches.config'
|
||||||
import { Input, Label } from "./Input";
|
import { Input, Label } from './Input'
|
||||||
import truncate from "../utils/truncate";
|
import truncate from '../utils/truncate'
|
||||||
|
|
||||||
const labelStyle = css({
|
const labelStyle = css({
|
||||||
color: "$mauve10",
|
color: '$mauve10',
|
||||||
textTransform: "uppercase",
|
textTransform: 'uppercase',
|
||||||
fontSize: "10px",
|
fontSize: '10px',
|
||||||
mb: "$0.5",
|
mb: '$0.5'
|
||||||
});
|
})
|
||||||
import transactionsData from "../content/transactions.json";
|
import transactionsData from '../content/transactions.json'
|
||||||
import { SetHookDialog } from "./SetHookDialog";
|
import { SetHookDialog } from './SetHookDialog'
|
||||||
import { addFunds } from "../state/actions/addFaucetAccount";
|
import { addFunds } from '../state/actions/addFaucetAccount'
|
||||||
import { deleteHook } from "../state/actions/deployHook";
|
import { deleteHook } from '../state/actions/deployHook'
|
||||||
|
import { capitalize } from '../utils/helpers'
|
||||||
|
import { deleteAccount } from '../state/actions/deleteAccount'
|
||||||
|
import { xrplSend } from '../state/actions/xrpl-client'
|
||||||
|
|
||||||
export const AccountDialog = ({
|
export const AccountDialog = ({
|
||||||
activeAccountAddress,
|
activeAccountAddress,
|
||||||
setActiveAccountAddress,
|
setActiveAccountAddress
|
||||||
}: {
|
}: {
|
||||||
activeAccountAddress: string | null;
|
activeAccountAddress: string | null
|
||||||
setActiveAccountAddress: React.Dispatch<React.SetStateAction<string | null>>;
|
setActiveAccountAddress: React.Dispatch<React.SetStateAction<string | null>>
|
||||||
}) => {
|
}) => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state)
|
||||||
const [showSecret, setShowSecret] = useState(false);
|
const [showSecret, setShowSecret] = useState(false)
|
||||||
const activeAccount = snap.accounts.find(
|
const activeAccount = snap.accounts.find(account => account.address === activeAccountAddress)
|
||||||
(account) => account.address === activeAccountAddress
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={Boolean(activeAccountAddress)}
|
open={Boolean(activeAccountAddress)}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={open => {
|
||||||
setShowSecret(false);
|
setShowSecret(false)
|
||||||
!open && setActiveAccountAddress(null);
|
!open && setActiveAccountAddress(null)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
css={{
|
css={{
|
||||||
backgroundColor: "$mauve1 !important",
|
backgroundColor: '$mauve1 !important',
|
||||||
border: "1px solid $mauve2",
|
border: '1px solid $mauve2',
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
// backgroundColor: "$black !important",
|
// backgroundColor: "$black !important",
|
||||||
},
|
},
|
||||||
p: "$3",
|
p: '$3',
|
||||||
"&:before": {
|
'&:before': {
|
||||||
content: " ",
|
content: ' ',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
opacity: 0.2,
|
opacity: 0.2,
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
opacity: 1,
|
opacity: 1
|
||||||
},
|
},
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none',
|
||||||
backgroundImage: `url('/pattern-dark.svg'), url('/pattern-dark-2.svg')`,
|
backgroundImage: `url('/pattern-dark.svg'), url('/pattern-dark-2.svg')`,
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: "bottom left, top right",
|
backgroundPosition: 'bottom left, top right'
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle
|
<DialogTitle
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
borderBottom: "1px solid $mauve6",
|
borderBottom: '1px solid $mauve6',
|
||||||
pb: "$3",
|
pb: '$3',
|
||||||
gap: "$3",
|
gap: '$3',
|
||||||
fontSize: "$md",
|
fontSize: '$md'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Wallet size="15px" /> {activeAccount?.name}
|
<Wallet size="15px" /> {activeAccount?.name}
|
||||||
@@ -95,164 +96,180 @@ export const AccountDialog = ({
|
|||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
outline
|
outline
|
||||||
css={{ ml: "auto", mr: "$9" }}
|
css={{ ml: 'auto', mr: '$9' }}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const index = state.accounts.findIndex(
|
deleteAccount(activeAccount?.address)
|
||||||
(acc) => acc.address === activeAccount?.address
|
|
||||||
);
|
|
||||||
state.accounts.splice(index, 1);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete Account <Trash size="15px" />
|
Delete Account <Trash size="15px" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription as="div" css={{ fontFamily: "$monospace" }}>
|
<DialogDescription as="div" css={{ fontFamily: '$monospace' }}>
|
||||||
<Stack css={{ display: "flex", flexDirection: "column", gap: "$3" }}>
|
<Stack css={{ display: 'flex', flexDirection: 'column', gap: '$3' }}>
|
||||||
<Flex css={{ alignItems: "center" }}>
|
<Flex css={{ alignItems: 'center' }}>
|
||||||
<Flex css={{ flexDirection: "column" }}>
|
<Flex css={{ flexDirection: 'column' }}>
|
||||||
<Text className={labelStyle()}>Account Address</Text>
|
<Text className={labelStyle()}>Account Address</Text>
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
|
a: { '&:hover': { textDecoration: 'underline' } }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeAccount?.address}
|
<a
|
||||||
|
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{activeAccount?.address}
|
||||||
|
</a>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
|
<Flex css={{ marginLeft: 'auto', color: '$mauve12' }}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
ghost
|
ghost
|
||||||
css={{ mt: "$3" }}
|
css={{ mt: '$3' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(activeAccount?.address || "");
|
navigator.clipboard.writeText(activeAccount?.address || '')
|
||||||
toast.success("Copied address to clipboard");
|
toast.success('Copied address to clipboard')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Copy size="15px" />
|
<Copy size="15px" />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex css={{ alignItems: "center" }}>
|
<Flex css={{ alignItems: 'center' }}>
|
||||||
<Flex css={{ flexDirection: "column" }}>
|
<Flex css={{ flexDirection: 'column' }}>
|
||||||
<Text className={labelStyle()}>Secret</Text>
|
<Text className={labelStyle()}>Secret</Text>
|
||||||
<Text
|
<Text
|
||||||
as="div"
|
as="div"
|
||||||
css={{
|
css={{
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showSecret
|
{showSecret
|
||||||
? activeAccount?.secret
|
? activeAccount?.secret
|
||||||
: "•".repeat(activeAccount?.secret.length || 16)}{" "}
|
: '•'.repeat(activeAccount?.secret.length || 16)}{' '}
|
||||||
<Button
|
<Button
|
||||||
css={{
|
css={{
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
mt: "2px",
|
mt: '2px',
|
||||||
ml: "$3",
|
ml: '$3'
|
||||||
}}
|
}}
|
||||||
ghost
|
ghost
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => setShowSecret((curr) => !curr)}
|
onClick={() => setShowSecret(curr => !curr)}
|
||||||
>
|
>
|
||||||
{showSecret ? "Hide" : "Show"}
|
{showSecret ? 'Hide' : 'Show'}
|
||||||
</Button>
|
</Button>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
|
<Flex css={{ marginLeft: 'auto', color: '$mauve12' }}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
ghost
|
ghost
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(activeAccount?.secret || "");
|
navigator.clipboard.writeText(activeAccount?.secret || '')
|
||||||
toast.success("Copied secret to clipboard");
|
toast.success('Copied secret to clipboard')
|
||||||
}}
|
}}
|
||||||
css={{ mt: "$3" }}
|
css={{ mt: '$3' }}
|
||||||
>
|
>
|
||||||
<Copy size="15px" />
|
<Copy size="15px" />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex css={{ alignItems: "center" }}>
|
<Flex css={{ alignItems: 'center' }}>
|
||||||
<Flex css={{ flexDirection: "column" }}>
|
<Flex css={{ flexDirection: 'column' }}>
|
||||||
<Text className={labelStyle()}>Balances & Objects</Text>
|
<Text className={labelStyle()}>Balances & Objects</Text>
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Dinero({
|
{Dinero({
|
||||||
amount: Number(activeAccount?.xrp || "0"),
|
amount: Number(activeAccount?.xrp || '0'),
|
||||||
precision: 6,
|
precision: 6
|
||||||
})
|
})
|
||||||
.toUnit()
|
.toUnit()
|
||||||
.toLocaleString(undefined, {
|
.toLocaleString(undefined, {
|
||||||
style: "currency",
|
style: 'currency',
|
||||||
currency: "XRP",
|
currency: 'XRP',
|
||||||
currencyDisplay: "name",
|
currencyDisplay: 'name'
|
||||||
})}
|
})}
|
||||||
<Button
|
<Button
|
||||||
css={{
|
css={{
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
lineHeight: 2,
|
lineHeight: 2,
|
||||||
mt: "2px",
|
mt: '2px',
|
||||||
ml: "$3",
|
ml: '$3'
|
||||||
}}
|
}}
|
||||||
ghost
|
ghost
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addFunds(activeAccount?.address || "");
|
addFunds(activeAccount?.address || '')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Funds
|
Add Funds
|
||||||
</Button>
|
</Button>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex css={{ marginLeft: "auto" }}>
|
<Flex
|
||||||
|
css={{
|
||||||
|
marginLeft: 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
|
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>
|
>
|
||||||
<Button
|
<Button size="sm" ghost css={{ color: '$grass11 !important', mt: '$3' }}>
|
||||||
size="sm"
|
|
||||||
ghost
|
|
||||||
css={{ color: "$grass11 !important", mt: "$3" }}
|
|
||||||
>
|
|
||||||
<ArrowSquareOut size="15px" />
|
<ArrowSquareOut size="15px" />
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex css={{ alignItems: "center" }}>
|
<Flex css={{ alignItems: 'center' }}>
|
||||||
<Flex css={{ flexDirection: "column" }}>
|
<Flex css={{ flexDirection: 'column' }}>
|
||||||
<Text className={labelStyle()}>Installed Hooks</Text>
|
<Text className={labelStyle()}>Installed Hooks</Text>
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
|
a: { '&:hover': { textDecoration: 'underline' } }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeAccount && activeAccount.hooks.length > 0
|
{activeAccount && activeAccount.hooks.length > 0
|
||||||
? activeAccount.hooks.map((i) => truncate(i, 12)).join(",")
|
? activeAccount.hooks.map(i => {
|
||||||
: "–"}
|
return (
|
||||||
|
<a
|
||||||
|
key={i}
|
||||||
|
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${i}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{truncate(i, 12)}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: '–'}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
{activeAccount && activeAccount?.hooks?.length > 0 && (
|
{activeAccount && activeAccount?.hooks?.length > 0 && (
|
||||||
<Flex css={{ marginLeft: "auto" }}>
|
<Flex css={{ marginLeft: 'auto' }}>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
outline
|
outline
|
||||||
disabled={activeAccount.isLoading}
|
disabled={activeAccount.isLoading}
|
||||||
css={{ mt: "$3", mr: "$1", ml: "auto" }}
|
css={{ mt: '$3', mr: '$1', ml: 'auto' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteHook(activeAccount);
|
deleteHook(activeAccount)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete Hook <Trash size="15px" />
|
Delete Hook <Trash size="15px" />
|
||||||
@@ -263,117 +280,109 @@ export const AccountDialog = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
|
||||||
<X size="20px" />
|
<X size="20px" />
|
||||||
</Box>
|
</Box>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
};
|
|
||||||
|
|
||||||
interface AccountProps {
|
|
||||||
card?: boolean;
|
|
||||||
hideDeployBtn?: boolean;
|
|
||||||
showHookStats?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Accounts: FC<AccountProps> = (props) => {
|
interface AccountProps {
|
||||||
const snap = useSnapshot(state);
|
card?: boolean
|
||||||
const [activeAccountAddress, setActiveAccountAddress] = useState<
|
hideDeployBtn?: boolean
|
||||||
string | null
|
showHookStats?: boolean
|
||||||
>(null);
|
}
|
||||||
|
|
||||||
|
const Accounts: FC<AccountProps> = props => {
|
||||||
|
const snap = useSnapshot(state)
|
||||||
|
const [activeAccountAddress, setActiveAccountAddress] = useState<string | null>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAccInfo = async () => {
|
const fetchAccInfo = async () => {
|
||||||
if (snap.clientStatus === "online") {
|
if (snap.clientStatus === 'online') {
|
||||||
const requests = snap.accounts.map((acc) =>
|
const requests = snap.accounts.map(acc =>
|
||||||
snap.client?.send({
|
xrplSend({
|
||||||
id: `hooks-builder-req-info-${acc.address}`,
|
id: `hooks-builder-req-info-${acc.address}`,
|
||||||
command: "account_info",
|
command: 'account_info',
|
||||||
account: acc.address,
|
account: acc.address
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
const responses = await Promise.all(requests);
|
const responses = await Promise.all(requests)
|
||||||
responses.forEach((res: any) => {
|
responses.forEach((res: any) => {
|
||||||
const address = res?.account_data?.Account as string;
|
const address = res?.account_data?.Account as string
|
||||||
const balance = res?.account_data?.Balance as string;
|
const balance = res?.account_data?.Balance as string
|
||||||
const sequence = res?.account_data?.Sequence as number;
|
const sequence = res?.account_data?.Sequence as number
|
||||||
const accountToUpdate = state.accounts.find(
|
const accountToUpdate = state.accounts.find(acc => acc.address === address)
|
||||||
(acc) => acc.address === address
|
|
||||||
);
|
|
||||||
if (accountToUpdate) {
|
if (accountToUpdate) {
|
||||||
accountToUpdate.xrp = balance;
|
accountToUpdate.xrp = balance
|
||||||
accountToUpdate.sequence = sequence;
|
accountToUpdate.sequence = sequence
|
||||||
accountToUpdate.error = null;
|
accountToUpdate.error = null
|
||||||
} else {
|
} else {
|
||||||
const oldAccount = state.accounts.find(
|
const oldAccount = state.accounts.find(acc => acc.address === res?.account)
|
||||||
(acc) => acc.address === res?.account
|
|
||||||
);
|
|
||||||
if (oldAccount) {
|
if (oldAccount) {
|
||||||
oldAccount.xrp = "0";
|
oldAccount.xrp = '0'
|
||||||
oldAccount.error = {
|
oldAccount.error = {
|
||||||
code: res?.error,
|
code: res?.error,
|
||||||
message: res?.error_message,
|
message: res?.error_message
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
const objectRequests = snap.accounts.map((acc) => {
|
const objectRequests = snap.accounts.map(acc => {
|
||||||
return snap.client?.send({
|
return xrplSend({
|
||||||
id: `hooks-builder-req-objects-${acc.address}`,
|
id: `hooks-builder-req-objects-${acc.address}`,
|
||||||
command: "account_objects",
|
command: 'account_objects',
|
||||||
account: acc.address,
|
account: acc.address
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
const objectResponses = await Promise.all(objectRequests);
|
const objectResponses = await Promise.all(objectRequests)
|
||||||
objectResponses.forEach((res: any) => {
|
objectResponses.forEach((res: any) => {
|
||||||
const address = res?.account as string;
|
const address = res?.account as string
|
||||||
const accountToUpdate = state.accounts.find(
|
const accountToUpdate = state.accounts.find(acc => acc.address === address)
|
||||||
(acc) => acc.address === address
|
|
||||||
);
|
|
||||||
if (accountToUpdate) {
|
if (accountToUpdate) {
|
||||||
accountToUpdate.hooks =
|
accountToUpdate.hooks =
|
||||||
res.account_objects
|
res.account_objects
|
||||||
.find((ac: any) => ac?.LedgerEntryType === "Hook")
|
.find((ac: any) => ac?.LedgerEntryType === 'Hook')
|
||||||
?.Hooks?.map((oo: any) => oo.Hook.HookHash) || [];
|
?.Hooks?.map((oo: any) => oo.Hook.HookHash) || []
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let fetchAccountInfoInterval: NodeJS.Timer;
|
let fetchAccountInfoInterval: NodeJS.Timer
|
||||||
if (snap.clientStatus === "online") {
|
if (snap.clientStatus === 'online') {
|
||||||
fetchAccInfo();
|
fetchAccInfo()
|
||||||
fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 10000);
|
fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 10000)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (snap.accounts.length > 0) {
|
if (snap.accounts.length > 0) {
|
||||||
if (fetchAccountInfoInterval) {
|
if (fetchAccountInfoInterval) {
|
||||||
clearInterval(fetchAccountInfoInterval);
|
clearInterval(fetchAccountInfoInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [snap.accounts.length, snap.clientStatus]);
|
}, [snap.accounts.length, snap.clientStatus])
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as="div"
|
as="div"
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
backgroundColor: props.card ? "$deep" : "$mauve1",
|
backgroundColor: props.card ? '$deep' : '$mauve1',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
flex: "1",
|
flex: '1',
|
||||||
height: "100%",
|
height: '100%',
|
||||||
border: "1px solid $mauve6",
|
border: '1px solid $mauve6',
|
||||||
borderRadius: props.card ? "$md" : undefined,
|
borderRadius: props.card ? '$md' : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container css={{ p: 0, flexShrink: 1, height: "100%" }}>
|
<Container css={{ p: 0, flexShrink: 1, height: '100%' }}>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
py: "$3",
|
py: '$3',
|
||||||
borderBottom: props.card ? "1px solid $mauve6" : undefined,
|
borderBottom: props.card ? '1px solid $mauve6' : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Heading
|
<Heading
|
||||||
@@ -381,82 +390,80 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
css={{
|
css={{
|
||||||
fontWeight: 300,
|
fontWeight: 300,
|
||||||
m: 0,
|
m: 0,
|
||||||
fontSize: "11px",
|
fontSize: '11px',
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
px: "$3",
|
px: '$3',
|
||||||
textTransform: "uppercase",
|
textTransform: 'uppercase',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
gap: "$3",
|
gap: '$3'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
|
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
|
<Flex css={{ ml: 'auto', gap: '$3', marginRight: '$3' }}>
|
||||||
<Button ghost size="sm" onClick={() => addFaucetAccount(true)}>
|
<ImportAccountDialog type="create" />
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
<ImportAccountDialog />
|
<ImportAccountDialog />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Stack
|
<Stack
|
||||||
css={{
|
css={{
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
fontSize: "13px",
|
fontSize: '13px',
|
||||||
wordWrap: "break-word",
|
wordWrap: 'break-word',
|
||||||
fontWeight: "$body",
|
fontWeight: '$body',
|
||||||
gap: 0,
|
gap: 0,
|
||||||
height: "calc(100% - 52px)",
|
height: 'calc(100% - 52px)',
|
||||||
flexWrap: "nowrap",
|
flexWrap: 'nowrap',
|
||||||
overflowY: "auto",
|
overflowY: 'auto'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{snap.accounts.map((account) => (
|
{snap.accounts.map(account => (
|
||||||
<Flex
|
<Flex
|
||||||
column
|
column
|
||||||
key={account.address + account.name}
|
key={account.address + account.name}
|
||||||
onClick={() => setActiveAccountAddress(account.address)}
|
onClick={() => setActiveAccountAddress(account.address)}
|
||||||
css={{
|
css={{
|
||||||
px: "$3",
|
px: '$3',
|
||||||
py: props.card ? "$3" : "$2",
|
py: props.card ? '$3' : '$2',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
borderBottom: props.card ? "1px solid $mauve6" : undefined,
|
borderBottom: props.card ? '1px solid $mauve6' : undefined,
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
background: "$backgroundAlt",
|
background: '$backgroundAlt'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
row
|
row
|
||||||
css={{
|
css={{
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Text>{account.name} </Text>
|
<Text>{account.name} </Text>
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
color: "$textMuted",
|
color: '$textMuted',
|
||||||
wordBreak: "break-word",
|
wordBreak: 'break-word'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{account.address}{" "}
|
{account.address}{' '}
|
||||||
{!account?.error ? (
|
{!account?.error ? (
|
||||||
`(${Dinero({
|
`(${Dinero({
|
||||||
amount: Number(account?.xrp || "0"),
|
amount: Number(account?.xrp || '0'),
|
||||||
precision: 6,
|
precision: 6
|
||||||
})
|
})
|
||||||
.toUnit()
|
.toUnit()
|
||||||
.toLocaleString(undefined, {
|
.toLocaleString(undefined, {
|
||||||
style: "currency",
|
style: 'currency',
|
||||||
currency: "XRP",
|
currency: 'XRP',
|
||||||
currencyDisplay: "name",
|
currencyDisplay: 'name'
|
||||||
})})`
|
})})`
|
||||||
) : (
|
) : (
|
||||||
<Box css={{ color: "$red11" }}>
|
<Box css={{ color: '$red11' }}>
|
||||||
(Account not found, request funds to activate account)
|
(Account not found, request funds to activate account)
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -465,8 +472,8 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
{!props.hideDeployBtn && (
|
{!props.hideDeployBtn && (
|
||||||
<div
|
<div
|
||||||
className="hook-deploy-button"
|
className="hook-deploy-button"
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SetHookDialog accountAddress={account.address} />
|
<SetHookDialog accountAddress={account.address} />
|
||||||
@@ -474,9 +481,9 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{props.showHookStats && (
|
{props.showHookStats && (
|
||||||
<Text muted small css={{ mt: "$2" }}>
|
<Text muted small css={{ mt: '$2' }}>
|
||||||
{account.hooks.length} hook
|
{account.hooks.length} hook
|
||||||
{account.hooks.length === 1 ? "" : "s"} installed
|
{account.hooks.length === 1 ? '' : 's'} installed
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -488,65 +495,95 @@ const Accounts: FC<AccountProps> = (props) => {
|
|||||||
setActiveAccountAddress={setActiveAccountAddress}
|
setActiveAccountAddress={setActiveAccountAddress}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const transactionsOptions = transactionsData.map((tx) => ({
|
export const transactionsOptions = transactionsData.map(tx => ({
|
||||||
value: tx.TransactionType,
|
value: tx.TransactionType,
|
||||||
label: tx.TransactionType,
|
label: tx.TransactionType
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const ImportAccountDialog = () => {
|
const ImportAccountDialog = ({ type = 'import' }: { type?: 'import' | 'create' }) => {
|
||||||
const [value, setValue] = useState("");
|
const [secret, setSecret] = useState('')
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
|
||||||
|
const btnText = type === 'import' ? 'Import' : 'Create'
|
||||||
|
const title = type === 'import' ? 'Import Account' : 'Create Account'
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (type === 'create') {
|
||||||
|
const value = capitalize(name)
|
||||||
|
await addFaucetAccount(value, true)
|
||||||
|
setName('')
|
||||||
|
setSecret('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
importAccount(secret, name)
|
||||||
|
setName('')
|
||||||
|
setSecret('')
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button ghost size="sm">
|
<Button ghost size="sm">
|
||||||
Import
|
{btnText}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent aria-describedby={undefined}>
|
||||||
<DialogTitle>Import account</DialogTitle>
|
<DialogTitle css={{ mb: '$4' }}>{title}</DialogTitle>
|
||||||
<DialogDescription>
|
<Flex column>
|
||||||
<Label>Add account secret</Label>
|
<Box css={{ mb: '$2' }}>
|
||||||
<Input
|
<Label>
|
||||||
name="secret"
|
Account name <Text muted>(optional)</Text>
|
||||||
type="password"
|
</Label>
|
||||||
value={value}
|
<Input
|
||||||
onChange={(e) => setValue(e.target.value)}
|
name="name"
|
||||||
/>
|
type="text"
|
||||||
</DialogDescription>
|
autoComplete="off"
|
||||||
|
autoCapitalize="on"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{type === 'import' && (
|
||||||
|
<Box>
|
||||||
|
<Label>Account secret</Label>
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
name="secret"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={secret}
|
||||||
|
onChange={e => setSecret(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
marginTop: 25,
|
marginTop: 25,
|
||||||
justifyContent: "flex-end",
|
justifyContent: 'flex-end',
|
||||||
gap: "$3",
|
gap: '$3'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button outline>Cancel</Button>
|
<Button outline>Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button type="submit" variant="primary" onClick={handleSubmit}>
|
||||||
variant="primary"
|
{title}
|
||||||
onClick={() => {
|
|
||||||
importAccount(value);
|
|
||||||
setValue("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Import account
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</Flex>
|
</Flex>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
|
||||||
<X size="20px" />
|
<X size="20px" />
|
||||||
</Box>
|
</Box>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Accounts;
|
export default Accounts
|
||||||
|
|||||||
@@ -1,65 +1,62 @@
|
|||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from 'react'
|
||||||
import { proxy, useSnapshot } from "valtio";
|
import { proxy, useSnapshot } from 'valtio'
|
||||||
import Button from "../Button";
|
import Button from '../Button'
|
||||||
import Flex from "../Flex";
|
import Flex from '../Flex'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle
|
||||||
} from "./primitive";
|
} from './primitive'
|
||||||
|
|
||||||
export interface AlertState {
|
export interface AlertState {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
title?: string;
|
title?: string
|
||||||
body?: ReactNode;
|
body?: ReactNode
|
||||||
cancelText?: string;
|
cancelText?: string
|
||||||
confirmText?: string;
|
confirmText?: string
|
||||||
confirmPrefix?: ReactNode;
|
confirmPrefix?: ReactNode
|
||||||
onConfirm?: () => any;
|
onConfirm?: () => any
|
||||||
onCancel?: () => any;
|
onCancel?: () => any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const alertState = proxy<AlertState>({
|
export const alertState = proxy<AlertState>({
|
||||||
isOpen: false,
|
isOpen: false
|
||||||
});
|
})
|
||||||
|
|
||||||
const Alert: FC = () => {
|
const Alert: FC = () => {
|
||||||
const {
|
const {
|
||||||
title = "Are you sure?",
|
title = 'Are you sure?',
|
||||||
isOpen,
|
isOpen,
|
||||||
body,
|
body,
|
||||||
cancelText,
|
cancelText,
|
||||||
confirmText = "Ok",
|
confirmText = 'Ok',
|
||||||
confirmPrefix,
|
confirmPrefix,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm
|
||||||
} = useSnapshot(alertState);
|
} = useSnapshot(alertState)
|
||||||
return (
|
return (
|
||||||
<AlertDialog
|
<AlertDialog open={isOpen} onOpenChange={value => (alertState.isOpen = value)}>
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={value => (alertState.isOpen = value)}
|
|
||||||
>
|
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>{body}</AlertDialogDescription>
|
<AlertDialogDescription>{body}</AlertDialogDescription>
|
||||||
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
|
<Flex css={{ justifyContent: 'flex-end', gap: '$3' }}>
|
||||||
{(cancelText || onCancel) && (
|
{(cancelText || onCancel) && (
|
||||||
<AlertDialogCancel asChild>
|
<AlertDialogCancel asChild>
|
||||||
<Button css={{ minWidth: "$16" }} outline onClick={onCancel}>
|
<Button css={{ minWidth: '$16' }} outline onClick={onCancel}>
|
||||||
{cancelText || "Cancel"}
|
{cancelText || 'Cancel'}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
)}
|
)}
|
||||||
<AlertDialogAction asChild>
|
<AlertDialogAction asChild>
|
||||||
<Button
|
<Button
|
||||||
css={{ minWidth: "$16" }}
|
css={{ minWidth: '$16' }}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await onConfirm?.();
|
await onConfirm?.()
|
||||||
alertState.isOpen = false;
|
alertState.isOpen = false
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{confirmPrefix}
|
{confirmPrefix}
|
||||||
@@ -69,7 +66,7 @@ const Alert: FC = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Alert;
|
export default Alert
|
||||||
|
|||||||
@@ -1,88 +1,83 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import { blackA } from "@radix-ui/colors";
|
import { blackA } from '@radix-ui/colors'
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||||
import { styled, keyframes } from "../../stitches.config";
|
import { styled, keyframes } from '../../stitches.config'
|
||||||
|
|
||||||
const overlayShow = keyframes({
|
const overlayShow = keyframes({
|
||||||
"0%": { opacity: 0 },
|
'0%': { opacity: 0 },
|
||||||
"100%": { opacity: 1 },
|
'100%': { opacity: 1 }
|
||||||
});
|
})
|
||||||
|
|
||||||
const contentShow = keyframes({
|
const contentShow = keyframes({
|
||||||
"0%": { opacity: 0, transform: "translate(-50%, -48%) scale(.96)" },
|
'0%': { opacity: 0, transform: 'translate(-50%, -48%) scale(.96)' },
|
||||||
"100%": { opacity: 1, transform: "translate(-50%, -50%) scale(1)" },
|
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
|
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
backgroundColor: blackA.blackA9,
|
backgroundColor: blackA.blackA9,
|
||||||
position: "fixed",
|
position: 'fixed',
|
||||||
inset: 0,
|
inset: 0,
|
||||||
"@media (prefers-reduced-motion: no-preference)": {
|
'@media (prefers-reduced-motion: no-preference)': {
|
||||||
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
|
||||||
},
|
},
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
backgroundColor: blackA.blackA11,
|
backgroundColor: blackA.blackA11
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const Root: React.FC<AlertDialogPrimitive.AlertDialogProps> = ({
|
const Root: React.FC<AlertDialogPrimitive.AlertDialogProps> = ({ children, ...rest }) => {
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Root {...rest}>
|
<AlertDialogPrimitive.Root {...rest}>
|
||||||
<StyledOverlay />
|
<StyledOverlay />
|
||||||
{children}
|
{children}
|
||||||
</AlertDialogPrimitive.Root>
|
</AlertDialogPrimitive.Root>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const StyledContent = styled(AlertDialogPrimitive.Content, {
|
const StyledContent = styled(AlertDialogPrimitive.Content, {
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
borderRadius: "$md",
|
borderRadius: '$md',
|
||||||
boxShadow:
|
boxShadow: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)',
|
||||||
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
|
position: 'fixed',
|
||||||
position: "fixed",
|
top: '50%',
|
||||||
top: "50%",
|
left: '50%',
|
||||||
left: "50%",
|
transform: 'translate(-50%, -50%)',
|
||||||
transform: "translate(-50%, -50%)",
|
width: '90vw',
|
||||||
width: "90vw",
|
maxWidth: '450px',
|
||||||
maxWidth: "450px",
|
maxHeight: '85vh',
|
||||||
maxHeight: "85vh",
|
|
||||||
padding: 25,
|
padding: 25,
|
||||||
"@media (prefers-reduced-motion: no-preference)": {
|
'@media (prefers-reduced-motion: no-preference)': {
|
||||||
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
|
||||||
},
|
},
|
||||||
"&:focus": { outline: "none" },
|
'&:focus': { outline: 'none' },
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
backgroundColor: "$mauve5",
|
backgroundColor: '$mauve5',
|
||||||
boxShadow:
|
boxShadow: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)'
|
||||||
"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, {
|
const StyledTitle = styled(AlertDialogPrimitive.Title, {
|
||||||
margin: 0,
|
margin: 0,
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
fontSize: "$lg",
|
fontSize: '$lg'
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledDescription = styled(AlertDialogPrimitive.Description, {
|
const StyledDescription = styled(AlertDialogPrimitive.Description, {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
color: "$mauve11",
|
color: '$mauve11',
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
fontSize: "$md",
|
fontSize: '$md'
|
||||||
});
|
})
|
||||||
|
|
||||||
// Exports
|
// Exports
|
||||||
export const AlertDialog = Root;
|
export const AlertDialog = Root
|
||||||
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
export const AlertDialogContent = StyledContent;
|
export const AlertDialogContent = StyledContent
|
||||||
export const AlertDialogTitle = StyledTitle;
|
export const AlertDialogTitle = StyledTitle
|
||||||
export const AlertDialogDescription = StyledDescription;
|
export const AlertDialogDescription = StyledDescription
|
||||||
export const AlertDialogAction = AlertDialogPrimitive.Action;
|
export const AlertDialogAction = AlertDialogPrimitive.Action
|
||||||
export const AlertDialogCancel = AlertDialogPrimitive.Cancel;
|
export const AlertDialogCancel = AlertDialogPrimitive.Cancel
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
const Box = styled("div", {
|
const Box = styled('div', {
|
||||||
// all: "unset",
|
// all: "unset",
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box'
|
||||||
});
|
})
|
||||||
|
|
||||||
export default Box;
|
export default Box
|
||||||
|
|||||||
@@ -1,291 +1,285 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
import Flex from "./Flex";
|
import Flex from './Flex'
|
||||||
import Spinner from "./Spinner";
|
import Spinner from './Spinner'
|
||||||
|
|
||||||
export const StyledButton = styled("button", {
|
export const StyledButton = styled('button', {
|
||||||
// Reset
|
// Reset
|
||||||
all: "unset",
|
all: 'unset',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
appereance: "none",
|
appereance: 'none',
|
||||||
fontFamily: "$body",
|
fontFamily: '$body',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box',
|
||||||
userSelect: "none",
|
userSelect: 'none',
|
||||||
"&::before": {
|
'&::before': {
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box'
|
||||||
},
|
},
|
||||||
"&::after": {
|
'&::after': {
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box'
|
||||||
},
|
},
|
||||||
// Custom reset?
|
// Custom reset?
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
lineHeight: "1",
|
lineHeight: '1',
|
||||||
gap: "5px",
|
gap: '5px',
|
||||||
WebkitTapHighlightColor: "rgba(0,0,0,0)",
|
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
|
||||||
// Custom
|
// Custom
|
||||||
height: "$6",
|
height: '$6',
|
||||||
px: "$2",
|
px: '$2',
|
||||||
fontSize: "$2",
|
fontSize: '$2',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
fontVariantNumeric: "tabular-nums",
|
fontVariantNumeric: 'tabular-nums',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
width: "max-content",
|
width: 'max-content',
|
||||||
"&:disabled": {
|
'&:disabled': {
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none',
|
||||||
cursor: "not-allowed",
|
cursor: 'not-allowed'
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
xs: {
|
xs: {
|
||||||
borderRadius: "$sm",
|
borderRadius: '$sm',
|
||||||
height: "$5",
|
height: '$5',
|
||||||
px: "$2",
|
px: '$2',
|
||||||
fontSize: "$xs",
|
fontSize: '$xs'
|
||||||
},
|
},
|
||||||
sm: {
|
sm: {
|
||||||
borderRadius: "$sm",
|
borderRadius: '$sm',
|
||||||
height: "$7",
|
height: '$7',
|
||||||
px: "$3",
|
px: '$3',
|
||||||
fontSize: "$xs",
|
fontSize: '$xs'
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
borderRadius: "$sm",
|
borderRadius: '$sm',
|
||||||
height: "$8",
|
height: '$8',
|
||||||
px: "$3",
|
px: '$3',
|
||||||
fontSize: "$xs",
|
fontSize: '$xs'
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
borderRadius: "$sm",
|
borderRadius: '$sm',
|
||||||
height: "$10",
|
height: '$10',
|
||||||
px: "$4",
|
px: '$4',
|
||||||
fontSize: "$xs",
|
fontSize: '$xs'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
link: {
|
link: {
|
||||||
textDecoration: "underline",
|
textDecoration: 'underline',
|
||||||
fontSize: "inherit",
|
fontSize: 'inherit',
|
||||||
color: "$textMuted",
|
color: '$textMuted',
|
||||||
textUnderlineOffset: "2px",
|
textUnderlineOffset: '2px'
|
||||||
},
|
},
|
||||||
default: {
|
default: {
|
||||||
backgroundColor: "$mauve12",
|
backgroundColor: '$mauve12',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$mauve12",
|
boxShadow: 'inset 0 0 0 1px $colors$mauve12',
|
||||||
color: "$mauve1",
|
color: '$mauve1',
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: "$mauve12",
|
backgroundColor: '$mauve12',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$mauve12",
|
boxShadow: 'inset 0 0 0 1px $colors$mauve12'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&:active": {
|
'&:active': {
|
||||||
backgroundColor: "$mauve10",
|
backgroundColor: '$mauve10',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$mauve11",
|
boxShadow: 'inset 0 0 0 1px $colors$mauve11'
|
||||||
},
|
},
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow:
|
boxShadow: 'inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12'
|
||||||
"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"]':
|
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
|
||||||
{
|
{
|
||||||
backgroundColor: "$mauve4",
|
backgroundColor: '$mauve4',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$mauve8",
|
boxShadow: 'inset 0 0 0 1px $colors$mauve8'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
backgroundColor: `$accent`,
|
backgroundColor: `$accent`,
|
||||||
boxShadow: "inset 0 0 0 1px $colors$purple9",
|
boxShadow: 'inset 0 0 0 1px $colors$purple9',
|
||||||
color: "$white",
|
color: '$white',
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: "$purple10",
|
backgroundColor: '$purple10',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$purple11",
|
boxShadow: 'inset 0 0 0 1px $colors$purple11'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&:active": {
|
'&:active': {
|
||||||
backgroundColor: "$purple8",
|
backgroundColor: '$purple8',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$purple8",
|
boxShadow: 'inset 0 0 0 1px $colors$purple8'
|
||||||
},
|
},
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow: "inset 0 0 0 2px $colors$purple12",
|
boxShadow: 'inset 0 0 0 2px $colors$purple12'
|
||||||
},
|
},
|
||||||
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
|
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
|
||||||
{
|
{
|
||||||
backgroundColor: "$mauve4",
|
backgroundColor: '$mauve4',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$purple8",
|
boxShadow: 'inset 0 0 0 1px $colors$purple8'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
backgroundColor: `$purple9`,
|
backgroundColor: `$purple9`,
|
||||||
boxShadow: "inset 0 0 0 1px $colors$purple9",
|
boxShadow: 'inset 0 0 0 1px $colors$purple9',
|
||||||
color: "$white",
|
color: '$white',
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: "$purple10",
|
backgroundColor: '$purple10',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$purple11",
|
boxShadow: 'inset 0 0 0 1px $colors$purple11'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&:active": {
|
'&:active': {
|
||||||
backgroundColor: "$purple8",
|
backgroundColor: '$purple8',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$purple8",
|
boxShadow: 'inset 0 0 0 1px $colors$purple8'
|
||||||
},
|
},
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow: "inset 0 0 0 2px $colors$purple12",
|
boxShadow: 'inset 0 0 0 2px $colors$purple12'
|
||||||
},
|
},
|
||||||
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
|
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
|
||||||
{
|
{
|
||||||
backgroundColor: "$mauve4",
|
backgroundColor: '$mauve4',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$purple8",
|
boxShadow: 'inset 0 0 0 1px $colors$purple8'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
destroy: {
|
destroy: {
|
||||||
backgroundColor: `$red9`,
|
backgroundColor: `$red9`,
|
||||||
boxShadow: "inset 0 0 0 1px $colors$red9",
|
boxShadow: 'inset 0 0 0 1px $colors$red9',
|
||||||
color: "$white",
|
color: '$white',
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: "$red10",
|
backgroundColor: '$red10',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$red11",
|
boxShadow: 'inset 0 0 0 1px $colors$red11'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&:active": {
|
'&:active': {
|
||||||
backgroundColor: "$red8",
|
backgroundColor: '$red8',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$red8",
|
boxShadow: 'inset 0 0 0 1px $colors$red8'
|
||||||
},
|
},
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow: "inset 0 0 0 2px $colors$red12",
|
boxShadow: 'inset 0 0 0 2px $colors$red12'
|
||||||
},
|
},
|
||||||
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
|
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
|
||||||
{
|
{
|
||||||
backgroundColor: "$mauve4",
|
backgroundColor: '$mauve4',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$red8",
|
boxShadow: 'inset 0 0 0 1px $colors$red8'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
true: {
|
true: {
|
||||||
color: "$textMuted",
|
color: '$textMuted'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
isDisabled: {
|
isDisabled: {
|
||||||
true: {
|
true: {
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
// pointerEvents: "none",
|
// pointerEvents: "none",
|
||||||
cursor: "auto",
|
cursor: 'auto',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
boxShadow: "inherit",
|
boxShadow: 'inherit'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
outline: {
|
outline: {
|
||||||
true: {
|
true: {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: 'transparent'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
uppercase: {
|
uppercase: {
|
||||||
true: {
|
true: {
|
||||||
textTransform: "uppercase",
|
textTransform: 'uppercase'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
fullWidth: {
|
fullWidth: {
|
||||||
true: {
|
true: {
|
||||||
width: "100%",
|
width: '100%'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
ghost: {
|
ghost: {
|
||||||
true: {
|
true: {
|
||||||
boxShadow: "none",
|
boxShadow: 'none',
|
||||||
background: "transparent",
|
background: 'transparent',
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: "$mauve6",
|
backgroundColor: '$mauve6',
|
||||||
boxShadow: "none",
|
boxShadow: 'none'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&:active": {
|
'&:active': {
|
||||||
backgroundColor: "$mauve8",
|
backgroundColor: '$mauve8',
|
||||||
boxShadow: "none",
|
boxShadow: 'none'
|
||||||
},
|
},
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow: "none",
|
boxShadow: 'none'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
isLoading: {
|
isLoading: {
|
||||||
true: {
|
true: {
|
||||||
"& .button-content": {
|
'& .button-content': {
|
||||||
visibility: "hidden",
|
visibility: 'hidden'
|
||||||
},
|
},
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
compoundVariants: [
|
||||||
{
|
{
|
||||||
outline: true,
|
outline: true,
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
css: {
|
css: {
|
||||||
background: "transparent",
|
background: 'transparent',
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
boxShadow: "inset 0 0 0 1px $colors$mauve10",
|
boxShadow: 'inset 0 0 0 1px $colors$mauve10',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
background: "$mauve5",
|
background: '$mauve5'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outline: true,
|
outline: true,
|
||||||
variant: "primary",
|
variant: 'primary',
|
||||||
css: {
|
css: {
|
||||||
background: "transparent",
|
background: 'transparent',
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
background: "$mauve5",
|
background: '$mauve5'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outline: true,
|
outline: true,
|
||||||
variant: "secondary",
|
variant: 'secondary',
|
||||||
css: {
|
css: {
|
||||||
background: "transparent",
|
background: 'transparent',
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
background: "$mauve5",
|
background: '$mauve5'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
size: "md",
|
size: 'md',
|
||||||
variant: "default",
|
variant: 'default'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const CustomButton: React.FC<
|
const CustomButton: React.FC<React.ComponentProps<typeof StyledButton> & { as?: string }> =
|
||||||
React.ComponentProps<typeof StyledButton> & { as?: string }
|
React.forwardRef(({ children, as = 'button', ...rest }, ref) => (
|
||||||
> = React.forwardRef(({ children, as = "button", ...rest }, ref) => (
|
// @ts-expect-error
|
||||||
// @ts-expect-error
|
<StyledButton {...rest} ref={ref} as={as}>
|
||||||
<StyledButton {...rest} ref={ref} as={as}>
|
<Flex as="span" css={{ gap: '$2', alignItems: 'center' }} className="button-content">
|
||||||
<Flex
|
{children}
|
||||||
as="span"
|
</Flex>
|
||||||
css={{ gap: "$2", alignItems: "center" }}
|
{rest.isLoading && <Spinner css={{ position: 'absolute' }} />}
|
||||||
className="button-content"
|
</StyledButton>
|
||||||
>
|
))
|
||||||
{children}
|
|
||||||
</Flex>
|
|
||||||
{rest.isLoading && <Spinner css={{ position: "absolute" }} />}
|
|
||||||
</StyledButton>
|
|
||||||
));
|
|
||||||
|
|
||||||
CustomButton.displayName = "CustomButton";
|
CustomButton.displayName = 'CustomButton'
|
||||||
|
|
||||||
export default CustomButton;
|
export default CustomButton
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
import { StyledButton } from "./Button";
|
import { StyledButton } from './Button'
|
||||||
|
|
||||||
const ButtonGroup = styled("div", {
|
const ButtonGroup = styled('div', {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
marginLeft: "1px",
|
marginLeft: '1px',
|
||||||
[`& ${StyledButton}`]: {
|
[`& ${StyledButton}`]: {
|
||||||
marginLeft: "-1px",
|
marginLeft: '-1px',
|
||||||
px: "$4",
|
px: '$4',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
"&:hover, &:focus": {
|
'&:hover, &:focus': {
|
||||||
zIndex: 200,
|
zIndex: 200
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
[`& ${StyledButton}:not(:only-of-type):not(:first-child):not(:last-child)`]: {
|
[`& ${StyledButton}:not(:only-of-type):not(:first-child):not(:last-child)`]: {
|
||||||
borderRadius: 0,
|
borderRadius: 0
|
||||||
},
|
},
|
||||||
[`& ${StyledButton}:first-child:not(:only-of-type)`]: {
|
[`& ${StyledButton}:first-child:not(:only-of-type)`]: {
|
||||||
borderBottomRightRadius: 0,
|
borderBottomRightRadius: 0,
|
||||||
borderTopRightRadius: 0,
|
borderTopRightRadius: 0
|
||||||
},
|
},
|
||||||
[`& ${StyledButton}:last-child:not(:only-of-type)`]: {
|
[`& ${StyledButton}:last-child:not(:only-of-type)`]: {
|
||||||
borderBottomLeftRadius: 0,
|
borderBottomLeftRadius: 0,
|
||||||
borderTopLeftRadius: 0,
|
borderTopLeftRadius: 0
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default ButtonGroup;
|
export default ButtonGroup
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
import Box from "./Box";
|
import Box from './Box'
|
||||||
|
|
||||||
const Container = styled(Box, {
|
const Container = styled(Box, {
|
||||||
width: "100%",
|
width: '100%',
|
||||||
marginLeft: "auto",
|
marginLeft: 'auto',
|
||||||
marginRight: "auto",
|
marginRight: 'auto',
|
||||||
px: "$4",
|
px: '$4',
|
||||||
maxWidth: "100%",
|
maxWidth: '100%'
|
||||||
});
|
})
|
||||||
|
|
||||||
export default Container;
|
export default Container
|
||||||
|
|||||||
121
components/ContextMenu/index.tsx
Normal file
121
components/ContextMenu/index.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { CaretRight, Check, Circle } from 'phosphor-react'
|
||||||
|
import { FC, Fragment, ReactNode } from 'react'
|
||||||
|
import { Flex, Text } from '../'
|
||||||
|
import {
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuItemIndicator,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuRoot,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuTriggerItem
|
||||||
|
} from './primitive'
|
||||||
|
|
||||||
|
export type TextOption = {
|
||||||
|
type: 'text'
|
||||||
|
label: ReactNode
|
||||||
|
onSelect?: () => any
|
||||||
|
children?: ContentMenuOption[]
|
||||||
|
}
|
||||||
|
export type SeparatorOption = { type: 'separator' }
|
||||||
|
export type CheckboxOption = {
|
||||||
|
type: 'checkbox'
|
||||||
|
label: ReactNode
|
||||||
|
checked?: boolean
|
||||||
|
onCheckedChange?: (isChecked: boolean) => any
|
||||||
|
}
|
||||||
|
export type RadioOption<T extends string = string> = {
|
||||||
|
type: 'radio'
|
||||||
|
label: ReactNode
|
||||||
|
onValueChange?: (value: string) => any
|
||||||
|
value: T
|
||||||
|
options?: { value: T; label?: ReactNode }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithCommons = { key: string; disabled?: boolean }
|
||||||
|
|
||||||
|
export type ContentMenuOption = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
|
||||||
|
WithCommons
|
||||||
|
|
||||||
|
export interface IContextMenu {
|
||||||
|
options?: ContentMenuOption[]
|
||||||
|
isNested?: boolean
|
||||||
|
}
|
||||||
|
export const ContextMenu: FC<IContextMenu> = ({ children, options, isNested }) => {
|
||||||
|
return (
|
||||||
|
<ContextMenuRoot>
|
||||||
|
{isNested ? (
|
||||||
|
<ContextMenuTriggerItem>{children}</ContextMenuTriggerItem>
|
||||||
|
) : (
|
||||||
|
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||||
|
)}
|
||||||
|
{options && !!options.length && (
|
||||||
|
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
|
||||||
|
{options.map(({ key, ...option }) => {
|
||||||
|
if (option.type === 'text') {
|
||||||
|
const { children, label, onSelect } = option
|
||||||
|
if (children)
|
||||||
|
return (
|
||||||
|
<ContextMenu isNested key={key} options={children}>
|
||||||
|
<Flex fluid row justify="space-between" align="center">
|
||||||
|
<Text>{label}</Text>
|
||||||
|
<CaretRight />
|
||||||
|
</Flex>
|
||||||
|
</ContextMenu>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<ContextMenuItem key={key} onSelect={onSelect}>
|
||||||
|
{label}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (option.type === 'checkbox') {
|
||||||
|
const { label, checked, onCheckedChange } = option
|
||||||
|
return (
|
||||||
|
<ContextMenuCheckboxItem
|
||||||
|
key={key}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
>
|
||||||
|
<Flex row align="center">
|
||||||
|
<ContextMenuItemIndicator>
|
||||||
|
<Check />
|
||||||
|
</ContextMenuItemIndicator>
|
||||||
|
<Text css={{ ml: checked ? '$4' : undefined }}>{label}</Text>
|
||||||
|
</Flex>
|
||||||
|
</ContextMenuCheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (option.type === 'radio') {
|
||||||
|
const { label, options, onValueChange, value } = option
|
||||||
|
return (
|
||||||
|
<Fragment key={key}>
|
||||||
|
<ContextMenuLabel>{label}</ContextMenuLabel>
|
||||||
|
<ContextMenuRadioGroup value={value} onValueChange={onValueChange}>
|
||||||
|
{options?.map(({ value: v, label }) => {
|
||||||
|
return (
|
||||||
|
<ContextMenuRadioItem key={v} value={v}>
|
||||||
|
<ContextMenuItemIndicator>
|
||||||
|
<Circle weight="fill" />
|
||||||
|
</ContextMenuItemIndicator>
|
||||||
|
<Text css={{ ml: '$4' }}>{label}</Text>
|
||||||
|
</ContextMenuRadioItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ContextMenuRadioGroup>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <ContextMenuSeparator key={key} />
|
||||||
|
})}
|
||||||
|
</ContextMenuContent>
|
||||||
|
)}
|
||||||
|
</ContextMenuRoot>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContextMenu
|
||||||
107
components/ContextMenu/primitive.tsx
Normal file
107
components/ContextMenu/primitive.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
|
||||||
|
import { styled } from '../../stitches.config'
|
||||||
|
import {
|
||||||
|
slideDownAndFade,
|
||||||
|
slideLeftAndFade,
|
||||||
|
slideRightAndFade,
|
||||||
|
slideUpAndFade
|
||||||
|
} from '../../styles/keyframes'
|
||||||
|
|
||||||
|
const StyledContent = styled(ContextMenuPrimitive.Content, {
|
||||||
|
minWidth: 140,
|
||||||
|
backgroundColor: '$backgroundOverlay',
|
||||||
|
borderRadius: 6,
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: '5px',
|
||||||
|
boxShadow:
|
||||||
|
'0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
|
||||||
|
'@media (prefers-reduced-motion: no-preference)': {
|
||||||
|
animationDuration: '400ms',
|
||||||
|
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
willChange: 'transform, opacity',
|
||||||
|
'&[data-state="open"]': {
|
||||||
|
'&[data-side="top"]': { animationName: slideDownAndFade },
|
||||||
|
'&[data-side="right"]': { animationName: slideLeftAndFade },
|
||||||
|
'&[data-side="bottom"]': { animationName: slideUpAndFade },
|
||||||
|
'&[data-side="left"]': { animationName: slideRightAndFade }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'.dark &': {
|
||||||
|
boxShadow:
|
||||||
|
'0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemStyles = {
|
||||||
|
all: 'unset',
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: '$text',
|
||||||
|
borderRadius: 3,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: 28,
|
||||||
|
padding: '0 7px',
|
||||||
|
position: 'relative',
|
||||||
|
paddingLeft: 10,
|
||||||
|
userSelect: 'none',
|
||||||
|
|
||||||
|
'&[data-disabled]': {
|
||||||
|
color: '$textMuted',
|
||||||
|
pointerEvents: 'none'
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:focus': {
|
||||||
|
backgroundColor: '$purple9',
|
||||||
|
color: '$white'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles })
|
||||||
|
const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, {
|
||||||
|
...itemStyles
|
||||||
|
})
|
||||||
|
const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, {
|
||||||
|
...itemStyles
|
||||||
|
})
|
||||||
|
const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, {
|
||||||
|
'&[data-state="open"]': {
|
||||||
|
backgroundColor: '$purple9',
|
||||||
|
color: '$purple9'
|
||||||
|
},
|
||||||
|
...itemStyles
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledLabel = styled(ContextMenuPrimitive.Label, {
|
||||||
|
paddingLeft: 10,
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '25px',
|
||||||
|
color: '$text'
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledSeparator = styled(ContextMenuPrimitive.Separator, {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: '$backgroundAlt',
|
||||||
|
margin: 5
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledItemIndicator = styled(ContextMenuPrimitive.ItemIndicator, {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
width: 25,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ContextMenuRoot = ContextMenuPrimitive.Root
|
||||||
|
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
||||||
|
export const ContextMenuContent = StyledContent
|
||||||
|
export const ContextMenuItem = StyledItem
|
||||||
|
export const ContextMenuCheckboxItem = StyledCheckboxItem
|
||||||
|
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
||||||
|
export const ContextMenuRadioItem = StyledRadioItem
|
||||||
|
export const ContextMenuItemIndicator = StyledItemIndicator
|
||||||
|
export const ContextMenuTriggerItem = StyledTriggerItem
|
||||||
|
export const ContextMenuLabel = StyledLabel
|
||||||
|
export const ContextMenuSeparator = StyledSeparator
|
||||||
@@ -1,38 +1,119 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useEffect } from 'react'
|
||||||
import { proxy, ref, useSnapshot } from "valtio";
|
import ReconnectingWebSocket, { CloseEvent } from 'reconnecting-websocket'
|
||||||
import { Select } from ".";
|
import { proxy, ref, useSnapshot } from 'valtio'
|
||||||
import state, { ILog, transactionsState } from "../state";
|
import { subscribeKey } from 'valtio/utils'
|
||||||
import { extractJSON } from "../utils/json";
|
import { Select } from '.'
|
||||||
import LogBox from "./LogBox";
|
import state, { ILog, transactionsState } from '../state'
|
||||||
|
import { extractJSON } from '../utils/json'
|
||||||
|
import EnrichLog from './EnrichLog'
|
||||||
|
import LogBox from './LogBox'
|
||||||
|
|
||||||
interface ISelect<T = string> {
|
interface ISelect<T = string> {
|
||||||
label: string;
|
label: string
|
||||||
value: T;
|
value: T
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStreamState {
|
export interface IStreamState {
|
||||||
selectedAccount: ISelect | null;
|
selectedAccount: ISelect | null
|
||||||
status: "idle" | "opened" | "closed";
|
status: 'idle' | 'opened' | 'closed'
|
||||||
statusChangeTimestamp?: number;
|
statusChangeTimestamp?: number
|
||||||
logs: ILog[];
|
logs: ILog[]
|
||||||
socket?: WebSocket;
|
socket?: ReconnectingWebSocket
|
||||||
}
|
}
|
||||||
|
|
||||||
export const streamState = proxy<IStreamState>({
|
export const streamState = proxy<IStreamState>({
|
||||||
selectedAccount: null as ISelect | null,
|
selectedAccount: null as ISelect | null,
|
||||||
status: "idle",
|
status: 'idle',
|
||||||
logs: [] as ILog[],
|
logs: [] as ILog[]
|
||||||
});
|
})
|
||||||
|
|
||||||
|
const onOpen = (account: ISelect | null) => {
|
||||||
|
if (!account) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// streamState.logs = [];
|
||||||
|
streamState.status = 'opened'
|
||||||
|
streamState.statusChangeTimestamp = Date.now()
|
||||||
|
|
||||||
|
pushLog(`Debug stream opened for account ${account?.value}`, {
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onError = () => {
|
||||||
|
pushLog('Something went wrong! Check your connection and try again.', {
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onClose = (e: CloseEvent) => {
|
||||||
|
// 999 = closed websocket connection by switching account
|
||||||
|
if (e.code !== 4999) {
|
||||||
|
pushLog(`Connection was closed. [code: ${e.code}]`, {
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
streamState.status = 'closed'
|
||||||
|
streamState.statusChangeTimestamp = Date.now()
|
||||||
|
}
|
||||||
|
const onMessage = (event: any) => {
|
||||||
|
// Ping returns just account address, if we get that
|
||||||
|
// response we don't need to log anything
|
||||||
|
if (event.data !== streamState.selectedAccount?.value) {
|
||||||
|
pushLog(event.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let interval: NodeJS.Timer | null = null
|
||||||
|
|
||||||
|
const addListeners = (account: ISelect | null) => {
|
||||||
|
if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
streamState.logs = []
|
||||||
|
if (account?.value) {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
if (streamState.socket) {
|
||||||
|
streamState.socket?.removeEventListener('open', () => onOpen(account))
|
||||||
|
streamState.socket?.removeEventListener('close', onClose)
|
||||||
|
streamState.socket?.removeEventListener('error', onError)
|
||||||
|
streamState.socket?.removeEventListener('message', onMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
streamState.socket = ref(
|
||||||
|
new ReconnectingWebSocket(
|
||||||
|
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (streamState.socket) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
streamState.socket?.send('')
|
||||||
|
}, 45000)
|
||||||
|
}
|
||||||
|
|
||||||
|
streamState.socket.addEventListener('open', () => onOpen(account))
|
||||||
|
streamState.socket.addEventListener('close', onClose)
|
||||||
|
streamState.socket.addEventListener('error', onError)
|
||||||
|
streamState.socket.addEventListener('message', onMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeKey(streamState, 'selectedAccount', addListeners)
|
||||||
|
|
||||||
|
const clearLog = () => {
|
||||||
|
streamState.logs = []
|
||||||
|
streamState.statusChangeTimestamp = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
const DebugStream = () => {
|
const DebugStream = () => {
|
||||||
const { selectedAccount, logs, socket } = useSnapshot(streamState);
|
const { selectedAccount, logs } = useSnapshot(streamState)
|
||||||
const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
|
const { activeHeader: activeTxTab } = useSnapshot(transactionsState)
|
||||||
const { accounts } = useSnapshot(state);
|
const { accounts } = useSnapshot(state)
|
||||||
|
|
||||||
const accountOptions = accounts.map(acc => ({
|
const accountOptions = accounts.map(acc => ({
|
||||||
label: acc.name,
|
label: acc.name,
|
||||||
value: acc.address,
|
value: acc.address
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const renderNav = () => (
|
const renderNav = () => (
|
||||||
<>
|
<>
|
||||||
@@ -42,176 +123,60 @@ const DebugStream = () => {
|
|||||||
options={accountOptions}
|
options={accountOptions}
|
||||||
hideSelectedOptions
|
hideSelectedOptions
|
||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
onChange={acc => (streamState.selectedAccount = acc as any)}
|
onChange={acc => {
|
||||||
css={{ width: "100%" }}
|
streamState.socket?.close(4999, 'Old connection closed because user switched account')
|
||||||
|
streamState.selectedAccount = acc as any
|
||||||
|
}}
|
||||||
|
css={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const account = selectedAccount?.value;
|
const account = transactionsState.transactions.find(tx => tx.header === activeTxTab)?.state
|
||||||
if (account && (!socket || !socket.url.endsWith(account))) {
|
.selectedAccount
|
||||||
socket?.close();
|
|
||||||
streamState.socket = ref(
|
|
||||||
new WebSocket(
|
|
||||||
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else if (!account && socket) {
|
|
||||||
socket.close();
|
|
||||||
streamState.socket = undefined;
|
|
||||||
}
|
|
||||||
}, [selectedAccount?.value, socket]);
|
|
||||||
|
|
||||||
const onMount = useCallback(async () => {
|
|
||||||
// deliberately using `proxy` values and not the `useSnapshot` ones to have no dep list
|
|
||||||
const acc = streamState.selectedAccount;
|
|
||||||
const status = streamState.status;
|
|
||||||
|
|
||||||
if (status === "opened" && acc) {
|
|
||||||
// fetch the missing ones
|
|
||||||
try {
|
|
||||||
const url = `https://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/recent/${acc?.value}`;
|
|
||||||
|
|
||||||
// TODO Remove after api sets cors properly
|
|
||||||
const res = await fetch("/api/proxy", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ url }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) return;
|
|
||||||
|
|
||||||
const body = await res.json();
|
|
||||||
|
|
||||||
if (!body?.logs) return;
|
|
||||||
|
|
||||||
const start = streamState.statusChangeTimestamp || 0;
|
|
||||||
streamState.logs = [];
|
|
||||||
pushLog(`Debug stream opened for account ${acc.value}`, {
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
|
|
||||||
const logs = Object.entries(body.logs).filter(([tm]) => +tm >= start);
|
|
||||||
|
|
||||||
logs.forEach(([tm, log]) => pushLog(log));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onMount();
|
|
||||||
}, [onMount]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const account = selectedAccount?.value;
|
|
||||||
const socket = streamState.socket;
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const onOpen = () => {
|
|
||||||
streamState.logs = [];
|
|
||||||
streamState.status = "opened";
|
|
||||||
streamState.statusChangeTimestamp = Date.now();
|
|
||||||
pushLog(`Debug stream opened for account ${account}`, {
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onError = () => {
|
|
||||||
pushLog("Something went wrong! Check your connection and try again.", {
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onClose = (e: CloseEvent) => {
|
|
||||||
pushLog(`Connection was closed. [code: ${e.code}]`, {
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
streamState.selectedAccount = null;
|
|
||||||
streamState.status = "closed";
|
|
||||||
streamState.statusChangeTimestamp = Date.now();
|
|
||||||
};
|
|
||||||
const onMessage = (event: any) => {
|
|
||||||
pushLog(event.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.addEventListener("open", onOpen);
|
|
||||||
socket.addEventListener("close", onClose);
|
|
||||||
socket.addEventListener("error", onError);
|
|
||||||
socket.addEventListener("message", onMessage);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.removeEventListener("open", onOpen);
|
|
||||||
socket.removeEventListener("close", onClose);
|
|
||||||
socket.removeEventListener("message", onMessage);
|
|
||||||
socket.removeEventListener("error", onError);
|
|
||||||
};
|
|
||||||
}, [selectedAccount?.value, socket]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const account = transactionsState.transactions.find(
|
|
||||||
tx => tx.header === activeTxTab
|
|
||||||
)?.state.selectedAccount;
|
|
||||||
|
|
||||||
if (account && account.value !== streamState.selectedAccount?.value)
|
if (account && account.value !== streamState.selectedAccount?.value)
|
||||||
streamState.selectedAccount = account;
|
streamState.selectedAccount = account
|
||||||
}, [activeTxTab]);
|
}, [activeTxTab])
|
||||||
|
|
||||||
const clearLog = () => {
|
|
||||||
streamState.logs = [];
|
|
||||||
streamState.statusChangeTimestamp = Date.now();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogBox
|
<LogBox enhanced renderNav={renderNav} title="Debug stream" logs={logs} clearLog={clearLog} />
|
||||||
enhanced
|
)
|
||||||
renderNav={renderNav}
|
}
|
||||||
title="Debug stream"
|
|
||||||
logs={logs}
|
|
||||||
clearLog={clearLog}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DebugStream;
|
export default DebugStream
|
||||||
|
|
||||||
export const pushLog = (
|
export const pushLog = (str: any, opts: Partial<Pick<ILog, 'type'>> = {}): ILog | undefined => {
|
||||||
str: any,
|
if (!str) return
|
||||||
opts: Partial<Pick<ILog, "type">> = {}
|
if (typeof str !== 'string') throw Error('Unrecognized debug log stream!')
|
||||||
): ILog | undefined => {
|
|
||||||
if (!str) return;
|
|
||||||
if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
|
|
||||||
|
|
||||||
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m);
|
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m)
|
||||||
const [_, tm, msg] = match || [];
|
const [_, tm, msg] = match || []
|
||||||
|
|
||||||
const timestamp = Date.parse(tm || "") || undefined;
|
const timestamp = Date.parse(tm || '') || undefined
|
||||||
const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString();
|
const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString()
|
||||||
|
|
||||||
const extracted = extractJSON(msg);
|
const extracted = extractJSON(msg)
|
||||||
const message = !extracted
|
const _message = !extracted ? msg : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1)
|
||||||
? msg
|
const message = ref(<EnrichLog str={_message} />)
|
||||||
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
|
|
||||||
|
|
||||||
const jsonData = extracted
|
const _jsonData = extracted ? JSON.stringify(extracted.result, null, 2) : undefined
|
||||||
? JSON.stringify(extracted.result, null, 2)
|
const jsonData = _jsonData ? ref(<EnrichLog str={_jsonData} />) : undefined
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (extracted?.result?.id?._Request?.includes("hooks-builder-req")) {
|
if (extracted?.result?.id?._Request?.includes('hooks-builder-req')) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type = "log" } = opts;
|
const { type = 'log' } = opts
|
||||||
const log: ILog = {
|
const log: ILog = {
|
||||||
type,
|
type,
|
||||||
message,
|
message,
|
||||||
timestring,
|
timestring,
|
||||||
jsonData,
|
jsonData,
|
||||||
defaultCollapsed: true,
|
defaultCollapsed: true
|
||||||
};
|
}
|
||||||
|
|
||||||
if (log) streamState.logs.push(log);
|
if (log) streamState.logs.push(log)
|
||||||
return log;
|
return log
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,84 +1,96 @@
|
|||||||
import React, { useRef, useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import { useSnapshot, ref } from "valtio";
|
import { useSnapshot } 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 ReactTimeAgo from "react-time-ago";
|
|
||||||
import filesize from "filesize";
|
|
||||||
|
|
||||||
import Box from "./Box";
|
import { useTheme } from 'next-themes'
|
||||||
import Container from "./Container";
|
import { useRouter } from 'next/router'
|
||||||
import dark from "../theme/editor/amy.json";
|
import NextLink from 'next/link'
|
||||||
import light from "../theme/editor/xcode_default.json";
|
import ReactTimeAgo from 'react-time-ago'
|
||||||
import state from "../state";
|
import filesize from 'filesize'
|
||||||
import wat from "../utils/wat-highlight";
|
|
||||||
|
|
||||||
import EditorNavigation from "./EditorNavigation";
|
import Box from './Box'
|
||||||
import { Button, Text, Link, Flex } from ".";
|
import Container from './Container'
|
||||||
|
import state from '../state'
|
||||||
|
import wat from '../utils/wat-highlight'
|
||||||
|
|
||||||
loader.config({
|
import EditorNavigation from './EditorNavigation'
|
||||||
paths: {
|
import { Button, Text, Link, Flex, Tabs, Tab } from '.'
|
||||||
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
|
import Monaco from './Monaco'
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
|
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024]
|
||||||
|
|
||||||
const DeployEditor = () => {
|
const DeployEditor = () => {
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
|
const snap = useSnapshot(state)
|
||||||
const snap = useSnapshot(state);
|
const router = useRouter()
|
||||||
const router = useRouter();
|
const { theme } = useTheme()
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false)
|
||||||
|
|
||||||
const activeFile = snap.files[snap.active];
|
const compiledFiles = snap.files.filter(file => file.compiledContent)
|
||||||
const compiledSize = activeFile?.compiledContent?.byteLength || 0;
|
const activeFile = compiledFiles[snap.activeWat]
|
||||||
|
|
||||||
|
const renderNav = () => (
|
||||||
|
<Tabs activeIndex={snap.activeWat} onChangeActive={idx => (state.activeWat = idx)}>
|
||||||
|
{compiledFiles.map((file, index) => {
|
||||||
|
return <Tab key={file.name} header={`${file.name}.wat`} />
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
|
||||||
|
const compiledSize = activeFile?.compiledContent?.byteLength || 0
|
||||||
const color =
|
const color =
|
||||||
compiledSize > FILESIZE_BREAKPOINTS[1]
|
compiledSize > FILESIZE_BREAKPOINTS[1]
|
||||||
? "$error"
|
? '$error'
|
||||||
: compiledSize > FILESIZE_BREAKPOINTS[0]
|
: compiledSize > FILESIZE_BREAKPOINTS[0]
|
||||||
? "$warning"
|
? '$warning'
|
||||||
: "$success";
|
: '$success'
|
||||||
|
|
||||||
|
const isContentChanged = activeFile && activeFile.compiledValueSnapshot !== activeFile.content
|
||||||
|
// const hasDeployErrors = activeFile && activeFile.containsErrors;
|
||||||
|
|
||||||
const CompiledStatView = activeFile && (
|
const CompiledStatView = activeFile && (
|
||||||
<Flex
|
<Flex
|
||||||
column
|
column
|
||||||
align="center"
|
align="center"
|
||||||
css={{
|
css={{
|
||||||
fontSize: "$sm",
|
fontSize: '$sm',
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
textAlign: "center",
|
textAlign: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex row align="center">
|
<Flex row align="center">
|
||||||
<Text css={{ mr: "$1" }}>
|
<Text css={{ mr: '$1' }}>Compiled {activeFile.name.split('.')[0] + '.wasm'}</Text>
|
||||||
Compiled {activeFile.name.split(".")[0] + ".wasm"}
|
{activeFile?.lastCompiled && <ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />}
|
||||||
</Text>
|
|
||||||
{activeFile?.lastCompiled && (
|
|
||||||
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
|
|
||||||
)}
|
|
||||||
{activeFile.compiledContent?.byteLength && (
|
{activeFile.compiledContent?.byteLength && (
|
||||||
<Text css={{ ml: "$2", color }}>
|
<Text css={{ ml: '$2', color }}>({filesize(activeFile.compiledContent.byteLength)})</Text>
|
||||||
({filesize(activeFile.compiledContent.byteLength)})
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{activeFile.compiledContent?.byteLength && activeFile.compiledContent?.byteLength >= 64000 && (
|
||||||
|
<Flex css={{ flexDirection: 'column', py: '$3', pb: '$1' }}>
|
||||||
|
<Text css={{ ml: '$2', color: '$error' }}>
|
||||||
|
File size is larger than 64kB, cannot set hook!
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
<Button variant="link" onClick={() => setShowContent(true)}>
|
<Button variant="link" onClick={() => setShowContent(true)}>
|
||||||
View as WAT-file
|
View as WAT-file
|
||||||
</Button>
|
</Button>
|
||||||
|
{isContentChanged && (
|
||||||
|
<Text warning>
|
||||||
|
File contents were changed after last compile, compile again to incorporate your latest
|
||||||
|
changes in the build.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
)
|
||||||
const NoContentView = !snap.loading && router.isReady && (
|
const NoContentView = !snap.loading && router.isReady && (
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
mt: "-60px",
|
mt: '-60px',
|
||||||
fontSize: "$sm",
|
fontSize: '$sm',
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
maxWidth: "300px",
|
maxWidth: '300px',
|
||||||
textAlign: "center",
|
textAlign: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{`You haven't compiled any files yet, compile files on `}
|
{`You haven't compiled any files yet, compile files on `}
|
||||||
@@ -86,28 +98,27 @@ const DeployEditor = () => {
|
|||||||
<Link as="a">develop view</Link>
|
<Link as="a">develop view</Link>
|
||||||
</NextLink>
|
</NextLink>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
)
|
||||||
const isContent =
|
|
||||||
snap.files?.filter((file) => file.compiledWatContent).length > 0 &&
|
const isContent = snap.files?.filter(file => file.compiledWatContent).length > 0 && router.isReady
|
||||||
router.isReady;
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
width: "100%",
|
width: '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditorNavigation showWat />
|
<EditorNavigation renderNav={renderNav} />
|
||||||
<Container
|
<Container
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
alignItems: "center",
|
alignItems: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isContent ? (
|
{!isContent ? (
|
||||||
@@ -115,37 +126,41 @@ const DeployEditor = () => {
|
|||||||
) : !showContent ? (
|
) : !showContent ? (
|
||||||
CompiledStatView
|
CompiledStatView
|
||||||
) : (
|
) : (
|
||||||
<Editor
|
<Monaco
|
||||||
className="hooks-editor"
|
className="hooks-editor"
|
||||||
defaultLanguage={"wat"}
|
defaultLanguage={'wat'}
|
||||||
language={"wat"}
|
language={'wat'}
|
||||||
path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
|
path={`file://tmp/c/${activeFile?.name}.wat`}
|
||||||
value={snap.files?.[snap.active]?.compiledWatContent || ""}
|
value={activeFile?.compiledWatContent || ''}
|
||||||
beforeMount={(monaco) => {
|
beforeMount={monaco => {
|
||||||
monaco.languages.register({ id: "wat" });
|
monaco.languages.register({ id: 'wat' })
|
||||||
monaco.languages.setLanguageConfiguration("wat", wat.config);
|
monaco.languages.setLanguageConfiguration('wat', wat.config)
|
||||||
monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
|
monaco.languages.setMonarchTokensProvider('wat', wat.tokens)
|
||||||
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) => {
|
onMount={editor => {
|
||||||
editorRef.current = editor;
|
|
||||||
editor.updateOptions({
|
editor.updateOptions({
|
||||||
glyphMargin: true,
|
glyphMargin: true,
|
||||||
readOnly: true,
|
readOnly: true
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
theme={theme === "dark" ? "dark" : "light"}
|
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||||
|
overlay={
|
||||||
|
<Flex
|
||||||
|
css={{
|
||||||
|
m: '$1',
|
||||||
|
ml: 'auto',
|
||||||
|
fontSize: '$sm',
|
||||||
|
color: '$textMuted'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link onClick={() => setShowContent(false)}>Exit editor mode</Link>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default DeployEditor;
|
export default DeployEditor
|
||||||
|
|||||||
@@ -1,90 +1,88 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import * as Stiches from "@stitches/react";
|
import * as Stiches from '@stitches/react'
|
||||||
import { keyframes } from "@stitches/react";
|
import { keyframes } from '@stitches/react'
|
||||||
import { blackA } from "@radix-ui/colors";
|
import { blackA } from '@radix-ui/colors'
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
const overlayShow = keyframes({
|
const overlayShow = keyframes({
|
||||||
"0%": { opacity: 0.01 },
|
'0%': { opacity: 0.01 },
|
||||||
"100%": { opacity: 1 },
|
'100%': { opacity: 1 }
|
||||||
});
|
})
|
||||||
|
|
||||||
const contentShow = keyframes({
|
const contentShow = keyframes({
|
||||||
"0%": { opacity: 0.01 },
|
'0%': { opacity: 0.01 },
|
||||||
"100%": { opacity: 1 },
|
'100%': { opacity: 1 }
|
||||||
});
|
})
|
||||||
const StyledOverlay = styled(DialogPrimitive.Overlay, {
|
const StyledOverlay = styled(DialogPrimitive.Overlay, {
|
||||||
zIndex: 9999,
|
zIndex: 3000,
|
||||||
backgroundColor: blackA.blackA9,
|
backgroundColor: blackA.blackA9,
|
||||||
position: "fixed",
|
position: 'fixed',
|
||||||
inset: 0,
|
inset: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
display: "grid",
|
display: 'grid',
|
||||||
placeItems: "center",
|
placeItems: 'center',
|
||||||
overflowY: "auto",
|
overflowY: 'auto',
|
||||||
"@media (prefers-reduced-motion: no-preference)": {
|
'@media (prefers-reduced-motion: no-preference)': {
|
||||||
animation: `${overlayShow} 250ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
animation: `${overlayShow} 250ms cubic-bezier(0.16, 1, 0.3, 1)`
|
||||||
},
|
},
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
backgroundColor: blackA.blackA11,
|
backgroundColor: blackA.blackA11
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledContent = styled(DialogPrimitive.Content, {
|
const StyledContent = styled(DialogPrimitive.Content, {
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
borderRadius: "$md",
|
borderRadius: '$md',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
mb: "15%",
|
mb: '15%',
|
||||||
boxShadow:
|
boxShadow: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)',
|
||||||
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
|
width: '90vw',
|
||||||
width: "90vw",
|
maxWidth: '450px',
|
||||||
maxWidth: "450px",
|
|
||||||
// maxHeight: "85vh",
|
// maxHeight: "85vh",
|
||||||
padding: 25,
|
padding: 25,
|
||||||
"@media (prefers-reduced-motion: no-preference)": {
|
'@media (prefers-reduced-motion: no-preference)': {
|
||||||
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
|
||||||
},
|
},
|
||||||
"&:focus": { outline: "none" },
|
'&:focus': { outline: 'none' },
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
backgroundColor: "$mauve5",
|
backgroundColor: '$mauve5',
|
||||||
boxShadow:
|
boxShadow: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)'
|
||||||
"0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
|
}
|
||||||
},
|
})
|
||||||
});
|
|
||||||
|
|
||||||
const Content: React.FC<{ css?: Stiches.CSS }> = ({ css, children }) => {
|
const Content: React.FC<{ css?: Stiches.CSS }> = ({ css, children }) => {
|
||||||
return (
|
return (
|
||||||
<StyledOverlay>
|
<StyledOverlay>
|
||||||
<StyledContent css={css}>{children}</StyledContent>
|
<StyledContent css={css}>{children}</StyledContent>
|
||||||
</StyledOverlay>
|
</StyledOverlay>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const StyledTitle = styled(DialogPrimitive.Title, {
|
const StyledTitle = styled(DialogPrimitive.Title, {
|
||||||
margin: 0,
|
margin: 0,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
fontSize: 17,
|
fontSize: 17
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledDescription = styled(DialogPrimitive.Description, {
|
const StyledDescription = styled(DialogPrimitive.Description, {
|
||||||
margin: "10px 0 10px",
|
margin: '10px 0 10px',
|
||||||
color: "$mauve11",
|
color: '$mauve11',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5
|
||||||
});
|
})
|
||||||
|
|
||||||
// Exports
|
// Exports
|
||||||
export const Dialog = styled(DialogPrimitive.Root);
|
export const Dialog = styled(DialogPrimitive.Root)
|
||||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
export const DialogTrigger = DialogPrimitive.Trigger
|
||||||
export const DialogContent = Content;
|
export const DialogContent = Content
|
||||||
export const DialogTitle = StyledTitle;
|
export const DialogTitle = StyledTitle
|
||||||
export const DialogDescription = StyledDescription;
|
export const DialogDescription = StyledDescription
|
||||||
export const DialogClose = DialogPrimitive.Close;
|
export const DialogClose = DialogPrimitive.Close
|
||||||
export const DialogPortal = DialogPrimitive.Portal;
|
export const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|||||||
@@ -1,135 +1,120 @@
|
|||||||
import { keyframes } from "@stitches/react";
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
||||||
|
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
import {
|
||||||
const slideUpAndFade = keyframes({
|
slideDownAndFade,
|
||||||
"0%": { opacity: 0, transform: "translateY(2px)" },
|
slideLeftAndFade,
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
slideRightAndFade,
|
||||||
});
|
slideUpAndFade
|
||||||
|
} from '../styles/keyframes'
|
||||||
const slideRightAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateX(-2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const slideDownAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateY(-2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const slideLeftAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateX(2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const StyledContent = styled(DropdownMenuPrimitive.Content, {
|
const StyledContent = styled(DropdownMenuPrimitive.Content, {
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
padding: 5,
|
padding: 5,
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
|
'0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
|
||||||
"@media (prefers-reduced-motion: no-preference)": {
|
'@media (prefers-reduced-motion: no-preference)': {
|
||||||
animationDuration: "400ms",
|
animationDuration: '400ms',
|
||||||
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
|
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
willChange: "transform, opacity",
|
willChange: 'transform, opacity',
|
||||||
'&[data-state="open"]': {
|
'&[data-state="open"]': {
|
||||||
'&[data-side="top"]': { animationName: slideDownAndFade },
|
'&[data-side="top"]': { animationName: slideDownAndFade },
|
||||||
'&[data-side="right"]': { animationName: slideLeftAndFade },
|
'&[data-side="right"]': { animationName: slideLeftAndFade },
|
||||||
'&[data-side="bottom"]': { animationName: slideUpAndFade },
|
'&[data-side="bottom"]': { animationName: slideUpAndFade },
|
||||||
'&[data-side="left"]': { animationName: slideRightAndFade },
|
'&[data-side="left"]': { animationName: slideRightAndFade }
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
backgroundColor: "$mauve5",
|
backgroundColor: '$mauve5',
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
|
'0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const itemStyles = {
|
const itemStyles = {
|
||||||
all: "unset",
|
all: 'unset',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
height: 32,
|
height: 32,
|
||||||
padding: "0 5px",
|
padding: '0 5px',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
paddingLeft: "5px",
|
paddingLeft: '5px',
|
||||||
userSelect: "none",
|
userSelect: 'none',
|
||||||
py: "$0.5",
|
py: '$0.5',
|
||||||
pr: "$2",
|
pr: '$2',
|
||||||
gap: "$2",
|
gap: '$2',
|
||||||
|
|
||||||
"&[data-disabled]": {
|
'&[data-disabled]': {
|
||||||
color: "$mauve9",
|
color: '$mauve9',
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none'
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
backgroundColor: "$purple9",
|
backgroundColor: '$purple9',
|
||||||
color: "$white",
|
color: '$white'
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const StyledItem = styled(DropdownMenuPrimitive.Item, { ...itemStyles });
|
const StyledItem = styled(DropdownMenuPrimitive.Item, { ...itemStyles })
|
||||||
const StyledCheckboxItem = styled(DropdownMenuPrimitive.CheckboxItem, {
|
const StyledCheckboxItem = styled(DropdownMenuPrimitive.CheckboxItem, {
|
||||||
...itemStyles,
|
...itemStyles
|
||||||
});
|
})
|
||||||
const StyledRadioItem = styled(DropdownMenuPrimitive.RadioItem, {
|
const StyledRadioItem = styled(DropdownMenuPrimitive.RadioItem, {
|
||||||
...itemStyles,
|
...itemStyles
|
||||||
});
|
})
|
||||||
const StyledTriggerItem = styled(DropdownMenuPrimitive.TriggerItem, {
|
const StyledTriggerItem = styled(DropdownMenuPrimitive.TriggerItem, {
|
||||||
'&[data-state="open"]': {
|
'&[data-state="open"]': {
|
||||||
backgroundColor: "$purple9",
|
backgroundColor: '$purple9',
|
||||||
color: "$purple9",
|
color: '$purple9'
|
||||||
},
|
},
|
||||||
...itemStyles,
|
...itemStyles
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledLabel = styled(DropdownMenuPrimitive.Label, {
|
const StyledLabel = styled(DropdownMenuPrimitive.Label, {
|
||||||
paddingLeft: 25,
|
paddingLeft: 25,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineHeight: "25px",
|
lineHeight: '25px',
|
||||||
color: "$mauve11",
|
color: '$mauve11'
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledSeparator = styled(DropdownMenuPrimitive.Separator, {
|
const StyledSeparator = styled(DropdownMenuPrimitive.Separator, {
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: "$mauve7",
|
backgroundColor: '$mauve7',
|
||||||
margin: 5,
|
margin: 5
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledItemIndicator = styled(DropdownMenuPrimitive.ItemIndicator, {
|
const StyledItemIndicator = styled(DropdownMenuPrimitive.ItemIndicator, {
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
width: 25,
|
width: 25,
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center'
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledArrow = styled(DropdownMenuPrimitive.Arrow, {
|
const StyledArrow = styled(DropdownMenuPrimitive.Arrow, {
|
||||||
fill: "$mauve2",
|
fill: '$mauve2',
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
fill: "$mauve5",
|
fill: '$mauve5'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Exports
|
// Exports
|
||||||
export const DropdownMenu = DropdownMenuPrimitive.Root;
|
export const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
export const DropdownMenuContent = StyledContent;
|
export const DropdownMenuContent = StyledContent
|
||||||
export const DropdownMenuItem = StyledItem;
|
export const DropdownMenuItem = StyledItem
|
||||||
export const DropdownMenuCheckboxItem = StyledCheckboxItem;
|
export const DropdownMenuCheckboxItem = StyledCheckboxItem
|
||||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
export const DropdownMenuRadioItem = StyledRadioItem;
|
export const DropdownMenuRadioItem = StyledRadioItem
|
||||||
export const DropdownMenuItemIndicator = StyledItemIndicator;
|
export const DropdownMenuItemIndicator = StyledItemIndicator
|
||||||
export const DropdownMenuTriggerItem = StyledTriggerItem;
|
export const DropdownMenuTriggerItem = StyledTriggerItem
|
||||||
export const DropdownMenuLabel = StyledLabel;
|
export const DropdownMenuLabel = StyledLabel
|
||||||
export const DropdownMenuSeparator = StyledSeparator;
|
export const DropdownMenuSeparator = StyledSeparator
|
||||||
export const DropdownMenuArrow = StyledArrow;
|
export const DropdownMenuArrow = StyledArrow
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useRef, ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
Plus,
|
|
||||||
Share,
|
Share,
|
||||||
DownloadSimple,
|
DownloadSimple,
|
||||||
Gear,
|
Gear,
|
||||||
@@ -11,296 +10,151 @@ import {
|
|||||||
CloudArrowUp,
|
CloudArrowUp,
|
||||||
CaretDown,
|
CaretDown,
|
||||||
User,
|
User,
|
||||||
FilePlus,
|
FilePlus
|
||||||
} from "phosphor-react";
|
} from 'phosphor-react'
|
||||||
import Image from "next/image";
|
import Image from 'next/image'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuArrow,
|
DropdownMenuArrow,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator
|
||||||
} from "./DropdownMenu";
|
} from './DropdownMenu'
|
||||||
import NewWindow from "react-new-window";
|
import NewWindow from 'react-new-window'
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from 'next-auth/react'
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from 'valtio'
|
||||||
import toast from "react-hot-toast";
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import {
|
import { syncToGist, updateEditorSettings, downloadAsZip } from '../state/actions'
|
||||||
createNewFile,
|
import state from '../state'
|
||||||
syncToGist,
|
import Box from './Box'
|
||||||
updateEditorSettings,
|
import Button from './Button'
|
||||||
downloadAsZip,
|
import Container from './Container'
|
||||||
} from "../state/actions";
|
|
||||||
import state from "../state";
|
|
||||||
import Box from "./Box";
|
|
||||||
import Button from "./Button";
|
|
||||||
import Container from "./Container";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogClose,
|
DialogClose
|
||||||
} from "./Dialog";
|
} from './Dialog'
|
||||||
import Flex from "./Flex";
|
import Flex from './Flex'
|
||||||
import Stack from "./Stack";
|
import Stack from './Stack'
|
||||||
import { Input, Label } from "./Input";
|
import { Input, Label } from './Input'
|
||||||
import Text from "./Text";
|
import Tooltip from './Tooltip'
|
||||||
import Tooltip from "./Tooltip";
|
import { showAlert } from '../state/actions/showAlert'
|
||||||
import { styled } from "../stitches.config";
|
|
||||||
import { showAlert } from "../state/actions/showAlert";
|
|
||||||
|
|
||||||
const ErrorText = styled(Text, {
|
const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
|
||||||
color: "$error",
|
const snap = useSnapshot(state)
|
||||||
mt: "$1",
|
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false)
|
||||||
display: "block",
|
const { data: session, status } = useSession()
|
||||||
});
|
const [popup, setPopUp] = useState(false)
|
||||||
|
const [editorSettings, setEditorSettings] = useState(snap.editorSettings)
|
||||||
|
|
||||||
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|
||||||
const snap = useSnapshot(state);
|
|
||||||
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
|
|
||||||
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
|
|
||||||
const [newfileError, setNewfileError] = useState<string | null>(null);
|
|
||||||
const [filename, setFilename] = useState("");
|
|
||||||
const { data: session, status } = useSession();
|
|
||||||
const [popup, setPopUp] = useState(false);
|
|
||||||
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session && session.user && popup) {
|
if (session && session.user && popup) {
|
||||||
setPopUp(false);
|
setPopUp(false)
|
||||||
}
|
}
|
||||||
}, [session, popup]);
|
}, [session, popup])
|
||||||
|
|
||||||
// when filename changes, reset error
|
|
||||||
useEffect(() => {
|
|
||||||
setNewfileError(null);
|
|
||||||
}, [filename, setNewfileError]);
|
|
||||||
|
|
||||||
const showNewGistAlert = () => {
|
const showNewGistAlert = () => {
|
||||||
showAlert("Are you sure?", {
|
showAlert('Are you sure?', {
|
||||||
body: (
|
body: (
|
||||||
<>
|
<>
|
||||||
This action will create new <strong>public</strong> Github Gist from
|
This action will create new <strong>public</strong> Github Gist from your current saved
|
||||||
your current saved files. You can delete gist anytime from your GitHub
|
files. You can delete gist anytime from your GitHub Gists page.
|
||||||
Gists page.
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cancelText: "Cancel",
|
cancelText: 'Cancel',
|
||||||
confirmText: "Create new Gist",
|
confirmText: 'Create new Gist',
|
||||||
confirmPrefix: <FilePlus size="15px" />,
|
confirmPrefix: <FilePlus size="15px" />,
|
||||||
onConfirm: () => syncToGist(session, true),
|
onConfirm: () => syncToGist(session, true)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const validateFilename = useCallback(
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
(filename: string): { error: string | null } => {
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
// check if filename already exists
|
|
||||||
if (!filename) {
|
|
||||||
return { error: "You need to add filename" };
|
|
||||||
}
|
|
||||||
if (snap.files.find(file => file.name === filename)) {
|
|
||||||
return { error: "Filename already exists." };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filename.includes(".") || filename[filename.length - 1] === ".") {
|
|
||||||
return { error: "Filename should include file extension" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for illegal characters
|
|
||||||
const ALPHA_NUMERICAL_REGEX = /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g;
|
|
||||||
if (!filename.match(ALPHA_NUMERICAL_REGEX)) {
|
|
||||||
return {
|
|
||||||
error: `Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-" and it needs to have file extension (e.g. ".c")`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { error: null };
|
|
||||||
},
|
|
||||||
[snap.files]
|
|
||||||
);
|
|
||||||
const handleConfirm = useCallback(() => {
|
|
||||||
// add default extension in case omitted
|
|
||||||
const chk = validateFilename(filename);
|
|
||||||
if (chk && chk.error) {
|
|
||||||
setNewfileError(`Error: ${chk.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsNewfileDialogOpen(false);
|
|
||||||
createNewFile(filename);
|
|
||||||
setFilename("");
|
|
||||||
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
|
|
||||||
|
|
||||||
const files = snap.files;
|
|
||||||
return (
|
return (
|
||||||
<Flex css={{ flexShrink: 0, gap: "$0" }}>
|
<Flex css={{ flexShrink: 0, gap: '$0' }}>
|
||||||
<Flex
|
<Flex
|
||||||
|
id="kissa"
|
||||||
|
ref={scrollRef}
|
||||||
css={{
|
css={{
|
||||||
overflowX: "scroll",
|
overflowX: 'scroll',
|
||||||
py: "$3",
|
overflowY: 'hidden',
|
||||||
|
py: '$3',
|
||||||
|
pb: '$0',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
"&::-webkit-scrollbar": {
|
'&::-webkit-scrollbar': {
|
||||||
height: 0,
|
height: '0.3em',
|
||||||
background: "transparent",
|
background: 'rgba(0,0,0,.0)'
|
||||||
},
|
},
|
||||||
|
'&::-webkit-scrollbar-gutter': 'stable',
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
backgroundColor: 'rgba(0,0,0,.2)',
|
||||||
|
outline: '0px',
|
||||||
|
borderRadius: '9999px'
|
||||||
|
},
|
||||||
|
scrollbarColor: 'rgba(0,0,0,.2) rgba(0,0,0,0)',
|
||||||
|
scrollbarGutter: 'stable',
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
'.dark &': {
|
||||||
|
'&::-webkit-scrollbar': {
|
||||||
|
background: 'rgba(0,0,0,.0)'
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-gutter': 'stable',
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
backgroundColor: 'rgba(255,255,255,.2)',
|
||||||
|
outline: '0px',
|
||||||
|
borderRadius: '9999px'
|
||||||
|
},
|
||||||
|
scrollbarColor: 'rgba(255,255,255,.2) rgba(0,0,0,0)',
|
||||||
|
scrollbarGutter: 'stable',
|
||||||
|
scrollbarWidth: 'thin'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onWheelCapture={e => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollLeft += e.deltaY
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container css={{ flex: 1 }}>
|
<Container css={{ flex: 1 }} ref={containerRef}>
|
||||||
<Stack
|
{renderNav?.()}
|
||||||
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={{
|
|
||||||
"&:hover": {
|
|
||||||
span: {
|
|
||||||
visibility: "visible",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{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>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
py: "$3",
|
py: '$3',
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
zIndex: 1,
|
zIndex: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container
|
<Container css={{ width: 'unset', display: 'flex', alignItems: 'center' }}>
|
||||||
css={{ width: "unset", display: "flex", alignItems: "center" }}
|
{status === 'authenticated' ? (
|
||||||
>
|
|
||||||
{status === "authenticated" ? (
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
borderRadius: "$full",
|
borderRadius: '$full',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
width: "$6",
|
width: '$6',
|
||||||
height: "$6",
|
height: '$6',
|
||||||
boxShadow: "0px 0px 0px 1px $colors$mauve11",
|
boxShadow: '0px 0px 0px 1px $colors$mauve11',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
mr: "$3",
|
mr: '$3',
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
boxShadow: "0px 0px 0px 1px $colors$mauve12",
|
boxShadow: '0px 0px 0px 1px $colors$mauve12'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={session?.user?.image || ""}
|
src={session?.user?.image || ''}
|
||||||
width="30px"
|
width="30px"
|
||||||
height="30px"
|
height="30px"
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
@@ -310,21 +164,16 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem disabled onClick={() => signOut()}>
|
<DropdownMenuItem disabled onClick={() => signOut()}>
|
||||||
<User size="16px" /> {session?.user?.username} (
|
<User size="16px" /> {session?.user?.username} ({session?.user.name})
|
||||||
{session?.user.name})
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() => window.open(`http://gist.github.com/${session?.user.username}`)}
|
||||||
window.open(
|
|
||||||
`http://gist.github.com/${session?.user.username}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<ArrowSquareOut size="16px" />
|
<ArrowSquareOut size="16px" />
|
||||||
Go to your Gist
|
Go to your Gist
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={() => signOut({ callbackUrl: "/" })}>
|
<DropdownMenuItem onClick={() => signOut({ callbackUrl: '/' })}>
|
||||||
<SignOut size="16px" /> Log out
|
<SignOut size="16px" /> Log out
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
@@ -332,48 +181,43 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button outline size="sm" css={{ mr: '$3' }} onClick={() => setPopUp(true)}>
|
||||||
outline
|
|
||||||
size="sm"
|
|
||||||
css={{ mr: "$3" }}
|
|
||||||
onClick={() => setPopUp(true)}
|
|
||||||
>
|
|
||||||
<GithubLogo size="16px" /> Login
|
<GithubLogo size="16px" /> Login
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
css={{
|
css={{
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
marginLeft: "auto",
|
marginLeft: 'auto',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
gap: "$0",
|
gap: '$0',
|
||||||
borderRadius: "$sm",
|
borderRadius: '$sm',
|
||||||
boxShadow: "inset 0px 0px 0px 1px $colors$mauve10",
|
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve10',
|
||||||
zIndex: 9,
|
zIndex: 9,
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
button: {
|
button: {
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
px: "$2",
|
px: '$2',
|
||||||
alignSelf: "flex-start",
|
alignSelf: 'flex-start',
|
||||||
boxShadow: "none",
|
boxShadow: 'none'
|
||||||
},
|
},
|
||||||
"button:not(:first-child):not(:last-child)": {
|
'button:not(:first-child):not(:last-child)': {
|
||||||
borderRight: 0,
|
borderRight: 0,
|
||||||
borderLeft: 0,
|
borderLeft: 0
|
||||||
},
|
},
|
||||||
"button:first-child": {
|
'button:first-child': {
|
||||||
borderTopLeftRadius: "$sm",
|
borderTopLeftRadius: '$sm',
|
||||||
borderBottomLeftRadius: "$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:last-child': {
|
||||||
|
borderTopRightRadius: '$sm',
|
||||||
|
borderBottomRightRadius: '$sm',
|
||||||
|
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve10',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve12'
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip content="Download as ZIP">
|
<Tooltip content="Download as ZIP">
|
||||||
@@ -382,7 +226,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
onClick={downloadAsZip}
|
onClick={downloadAsZip}
|
||||||
outline
|
outline
|
||||||
size="sm"
|
size="sm"
|
||||||
css={{ alignItems: "center" }}
|
css={{ alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<DownloadSimple size="16px" />
|
<DownloadSimple size="16px" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -391,12 +235,10 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
size="sm"
|
size="sm"
|
||||||
css={{ alignItems: "center" }}
|
css={{ alignItems: 'center' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(`${window.location.origin}/develop/${snap.gistId}`)
|
||||||
`${window.location.origin}/develop/${snap.gistId}`
|
toast.success('Copied share link to clipboard!')
|
||||||
);
|
|
||||||
toast.success("Copied share link to clipboard!");
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Share size="16px" />
|
<Share size="16px" />
|
||||||
@@ -406,9 +248,9 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
content={
|
content={
|
||||||
session && session.user
|
session && session.user
|
||||||
? snap.gistOwner === session?.user.username
|
? snap.gistOwner === session?.user.username
|
||||||
? "Sync to Gist"
|
? 'Sync to Gist'
|
||||||
: "Create as a new Gist"
|
: 'Create as a new Gist'
|
||||||
: "You need to be logged in to sync with Gist"
|
: 'You need to be logged in to sync with Gist'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -416,15 +258,15 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={!session || !session.user}
|
isDisabled={!session || !session.user}
|
||||||
isLoading={snap.gistLoading}
|
isLoading={snap.gistLoading}
|
||||||
css={{ alignItems: "center" }}
|
css={{ alignItems: 'center' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (snap.gistOwner === session?.user.username) {
|
if (snap.gistOwner === session?.user.username) {
|
||||||
syncToGist(session);
|
syncToGist(session)
|
||||||
} else {
|
} else {
|
||||||
showNewGistAlert();
|
showNewGistAlert()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -443,38 +285,33 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem disabled={snap.zipLoading} onClick={downloadAsZip}>
|
||||||
disabled={snap.zipLoading}
|
|
||||||
onClick={downloadAsZip}
|
|
||||||
>
|
|
||||||
<DownloadSimple size="16px" /> Download as ZIP
|
<DownloadSimple size="16px" /> Download as ZIP
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
`${window.location.origin}/develop/${snap.gistId}`
|
`${window.location.origin}/develop/${snap.gistId}`
|
||||||
);
|
)
|
||||||
toast.success("Copied share link to clipboard!");
|
toast.success('Copied share link to clipboard!')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Share size="16px" />
|
<Share size="16px" />
|
||||||
Copy share link to clipboard
|
Copy share link to clipboard
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={
|
disabled={session?.user.username !== snap.gistOwner || !snap.gistId}
|
||||||
session?.user.username !== snap.gistOwner || !snap.gistId
|
|
||||||
}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
syncToGist(session);
|
syncToGist(session)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloudArrowUp size="16px" /> Push to Gist
|
<CloudArrowUp size="16px" /> Push to Gist
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={status !== "authenticated"}
|
disabled={status !== 'authenticated'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showNewGistAlert();
|
showNewGistAlert()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FilePlus size="16px" /> Create as a new Gist
|
<FilePlus size="16px" /> Create as a new Gist
|
||||||
@@ -489,9 +326,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{popup && !session ? (
|
{popup && !session ? <NewWindow center="parent" url="/sign-in" /> : null}
|
||||||
<NewWindow center="parent" url="/sign-in" />
|
|
||||||
) : null}
|
|
||||||
</Container>
|
</Container>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@@ -512,39 +347,33 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
onChange={e =>
|
onChange={e =>
|
||||||
setEditorSettings(curr => ({
|
setEditorSettings(curr => ({
|
||||||
...curr,
|
...curr,
|
||||||
tabSize: Number(e.target.value),
|
tabSize: Number(e.target.value)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
<Flex css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}>
|
<Flex css={{ marginTop: 25, justifyContent: 'flex-end', gap: '$3' }}>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button outline onClick={() => updateEditorSettings(editorSettings)}>
|
||||||
outline
|
|
||||||
onClick={() => updateEditorSettings(editorSettings)}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button variant="primary" onClick={() => updateEditorSettings(editorSettings)}>
|
||||||
variant="primary"
|
|
||||||
onClick={() => updateEditorSettings(editorSettings)}
|
|
||||||
>
|
|
||||||
Save changes
|
Save changes
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</Flex>
|
</Flex>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
|
||||||
<X size="20px" />
|
<X size="20px" />
|
||||||
</Box>
|
</Box>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default EditorNavigation;
|
export default EditorNavigation
|
||||||
|
|||||||
73
components/EnrichLog.tsx
Normal file
73
components/EnrichLog.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import regexifyString from 'regexify-string'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { Link } from '.'
|
||||||
|
import state from '../state'
|
||||||
|
import { AccountDialog } from './Accounts'
|
||||||
|
import Tooltip from './Tooltip'
|
||||||
|
import hookSetCodes from '../content/hook-set-codes.json'
|
||||||
|
import { capitalize } from '../utils/helpers'
|
||||||
|
|
||||||
|
interface EnrichLogProps {
|
||||||
|
str?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnrichLog: FC<EnrichLogProps> = ({ str }) => {
|
||||||
|
const { accounts } = useSnapshot(state)
|
||||||
|
const [dialogAccount, setDialogAccount] = useState<string | null>(null)
|
||||||
|
if (!str || !accounts.length) return <>{str}</>
|
||||||
|
|
||||||
|
const addrs = accounts.map(acc => acc.address)
|
||||||
|
const regex = `(${addrs.join('|')}|HookSet\\(\\d+\\))`
|
||||||
|
const res = regexifyString({
|
||||||
|
pattern: new RegExp(regex, 'gim'),
|
||||||
|
decorator: (match, idx) => {
|
||||||
|
if (match.startsWith('r')) {
|
||||||
|
// Account
|
||||||
|
const name = accounts.find(acc => acc.address === match)?.name
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={match + idx}
|
||||||
|
as="a"
|
||||||
|
onClick={() => setDialogAccount(match)}
|
||||||
|
title={match}
|
||||||
|
highlighted
|
||||||
|
>
|
||||||
|
{name || match}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (match.startsWith('HookSet')) {
|
||||||
|
const code = match.match(/^HookSet\((\d+)\)/)?.[1]
|
||||||
|
const val = hookSetCodes.find(v => code && v.code === +code)
|
||||||
|
console.log({ code, val })
|
||||||
|
if (!val) return match
|
||||||
|
|
||||||
|
const content = capitalize(val.description) || 'No hint available!'
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
HookSet(
|
||||||
|
<Tooltip content={content}>
|
||||||
|
<Link>{val.identifier}</Link>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
},
|
||||||
|
input: str
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{res}
|
||||||
|
<AccountDialog
|
||||||
|
setActiveAccountAddress={setDialogAccount}
|
||||||
|
activeAccountAddress={dialogAccount}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnrichLog
|
||||||
@@ -1,53 +1,53 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
import Box from "./Box";
|
import Box from './Box'
|
||||||
|
|
||||||
const Flex = styled(Box, {
|
const Flex = styled(Box, {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
variants: {
|
variants: {
|
||||||
row: {
|
row: {
|
||||||
true: {
|
true: {
|
||||||
flexDirection: "row",
|
flexDirection: 'row'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
true: {
|
true: {
|
||||||
flexDirection: "column",
|
flexDirection: 'column'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
fluid: {
|
fluid: {
|
||||||
true: {
|
true: {
|
||||||
width: "100%",
|
width: '100%'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
align: {
|
align: {
|
||||||
start: {
|
start: {
|
||||||
alignItems: "start",
|
alignItems: 'start'
|
||||||
},
|
},
|
||||||
center: {
|
center: {
|
||||||
alignItems: "center",
|
alignItems: 'center'
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
alignItems: "end",
|
alignItems: 'end'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
justify: {
|
justify: {
|
||||||
start: {
|
start: {
|
||||||
justifyContent: "start",
|
justifyContent: 'start'
|
||||||
},
|
},
|
||||||
center: {
|
center: {
|
||||||
justifyContent: "center",
|
justifyContent: 'center'
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
justifyContent: "end",
|
justifyContent: 'end'
|
||||||
},
|
},
|
||||||
"space-between": {
|
'space-between': {
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between'
|
||||||
},
|
},
|
||||||
"space-around": {
|
'space-around': {
|
||||||
justifyContent: "space-around",
|
justifyContent: 'space-around'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default Flex;
|
export default Flex
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
const Heading = styled("span", {
|
const Heading = styled('span', {
|
||||||
fontFamily: "$heading",
|
fontFamily: '$heading',
|
||||||
lineHeight: "$heading",
|
lineHeight: '$heading',
|
||||||
fontWeight: "$heading",
|
fontWeight: '$heading',
|
||||||
variants: {
|
variants: {
|
||||||
uppercase: {
|
uppercase: {
|
||||||
true: {
|
true: {
|
||||||
textTransform: "uppercase",
|
textTransform: 'uppercase'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default Heading;
|
export default Heading
|
||||||
|
|||||||
@@ -1,38 +1,45 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useSnapshot, ref } from "valtio";
|
import { useSnapshot, ref } from 'valtio'
|
||||||
import Editor from "@monaco-editor/react";
|
import type monaco from 'monaco-editor'
|
||||||
import type monaco from "monaco-editor";
|
import { ArrowBendLeftUp } from 'phosphor-react'
|
||||||
import { ArrowBendLeftUp } from "phosphor-react";
|
import { useTheme } from 'next-themes'
|
||||||
import { useTheme } from "next-themes";
|
import { useRouter } from 'next/router'
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import Box from "./Box";
|
import Box from './Box'
|
||||||
import Container from "./Container";
|
import Container from './Container'
|
||||||
import dark from "../theme/editor/amy.json";
|
import { createNewFile, saveFile } from '../state/actions'
|
||||||
import light from "../theme/editor/xcode_default.json";
|
import { apiHeaderFiles } from '../state/constants'
|
||||||
import { saveFile } from "../state/actions";
|
import state from '../state'
|
||||||
import { apiHeaderFiles } from "../state/constants";
|
|
||||||
import state from "../state";
|
|
||||||
|
|
||||||
import EditorNavigation from "./EditorNavigation";
|
import EditorNavigation from './EditorNavigation'
|
||||||
import Text from "./Text";
|
import Text from './Text'
|
||||||
import { MonacoServices } from "@codingame/monaco-languageclient";
|
import { MonacoServices } from '@codingame/monaco-languageclient'
|
||||||
import { createLanguageClient, createWebSocket } from "../utils/languageClient";
|
import { createLanguageClient, createWebSocket } from '../utils/languageClient'
|
||||||
import { listen } from "@codingame/monaco-jsonrpc";
|
import { listen } from '@codingame/monaco-jsonrpc'
|
||||||
import ReconnectingWebSocket from "reconnecting-websocket";
|
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||||
|
|
||||||
import docs from "../xrpl-hooks-docs/docs";
|
import docs from '../xrpl-hooks-docs/docs'
|
||||||
|
import Monaco from './Monaco'
|
||||||
|
import { saveAllFiles } from '../state/actions/saveFile'
|
||||||
|
import { Tab, Tabs } from './Tabs'
|
||||||
|
import { renameFile } from '../state/actions/createNewFile'
|
||||||
|
import { Link } from '.'
|
||||||
|
import Markdown from './Markdown'
|
||||||
|
|
||||||
|
const checkWritable = (filename?: string): boolean => {
|
||||||
|
if (apiHeaderFiles.find(file => file === filename)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||||
const currPath = editor.getModel()?.uri.path;
|
const filename = editor.getModel()?.uri.path.split('/').pop()
|
||||||
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
|
const isWritable = checkWritable(filename)
|
||||||
editor.updateOptions({ readOnly: true });
|
editor.updateOptions({ readOnly: !isWritable })
|
||||||
} else {
|
}
|
||||||
editor.updateOptions({ readOnly: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let decorations: { [key: string]: string[] } = {};
|
let decorations: { [key: string]: string[] } = {}
|
||||||
|
|
||||||
const setMarkers = (monacoE: typeof monaco) => {
|
const setMarkers = (monacoE: typeof monaco) => {
|
||||||
// Get all the markers that are active at the moment,
|
// Get all the markers that are active at the moment,
|
||||||
@@ -42,11 +49,11 @@ const setMarkers = (monacoE: typeof monaco) => {
|
|||||||
.getModelMarkers({})
|
.getModelMarkers({})
|
||||||
// Filter out the markers that are hooks specific
|
// Filter out the markers that are hooks specific
|
||||||
.filter(
|
.filter(
|
||||||
(marker) =>
|
marker =>
|
||||||
typeof marker?.code === "string" &&
|
typeof marker?.code === 'string' &&
|
||||||
// Take only markers that starts with "hooks-"
|
// Take only markers that starts with "hooks-"
|
||||||
marker?.code?.includes("hooks-")
|
marker?.code?.includes('hooks-')
|
||||||
);
|
)
|
||||||
|
|
||||||
// Get the active model (aka active file you're editing)
|
// Get the active model (aka active file you're editing)
|
||||||
// const model = monacoE.editor?.getModel(
|
// const model = monacoE.editor?.getModel(
|
||||||
@@ -55,17 +62,15 @@ const setMarkers = (monacoE: typeof monaco) => {
|
|||||||
// console.log(state.active);
|
// console.log(state.active);
|
||||||
// Add decoration (aka extra hoverMessages) to markers in the
|
// Add decoration (aka extra hoverMessages) to markers in the
|
||||||
// exact same range (location) where the markers are
|
// exact same range (location) where the markers are
|
||||||
const models = monacoE.editor.getModels();
|
const models = monacoE.editor.getModels()
|
||||||
models.forEach((model) => {
|
models.forEach(model => {
|
||||||
decorations[model.id] = model?.deltaDecorations(
|
decorations[model.id] = model?.deltaDecorations(
|
||||||
decorations?.[model.id] || [],
|
decorations?.[model.id] || [],
|
||||||
markers
|
markers
|
||||||
.filter((marker) =>
|
.filter(marker =>
|
||||||
marker?.resource.path
|
marker?.resource.path.split('/').includes(`${state.files?.[state.active]?.name}`)
|
||||||
.split("/")
|
|
||||||
.includes(`${state.files?.[state.active]?.name}`)
|
|
||||||
)
|
)
|
||||||
.map((marker) => ({
|
.map(marker => ({
|
||||||
range: new monacoE.Range(
|
range: new monacoE.Range(
|
||||||
marker.startLineNumber,
|
marker.startLineNumber,
|
||||||
marker.startColumn,
|
marker.startColumn,
|
||||||
@@ -79,191 +84,239 @@ const setMarkers = (monacoE: typeof monaco) => {
|
|||||||
// /xrpl-hooks-docs/xrpl-hooks-docs-files.json file
|
// /xrpl-hooks-docs/xrpl-hooks-docs-files.json file
|
||||||
// which was generated from rst files
|
// which was generated from rst files
|
||||||
|
|
||||||
(typeof marker.code === "string" &&
|
(typeof marker.code === 'string' && docs[marker?.code]?.toString()) || '',
|
||||||
docs[marker?.code]?.toString()) ||
|
|
||||||
"",
|
|
||||||
supportHtml: true,
|
supportHtml: true,
|
||||||
isTrusted: true,
|
isTrusted: true
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}))
|
}))
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const HooksEditor = () => {
|
const HooksEditor = () => {
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>()
|
||||||
const monacoRef = useRef<typeof monaco>();
|
const monacoRef = useRef<typeof monaco>()
|
||||||
const subscriptionRef = useRef<ReconnectingWebSocket | null>(null);
|
const subscriptionRef = useRef<ReconnectingWebSocket | null>(null)
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state)
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme()
|
||||||
|
const [isMdPreview, setIsMdPreview] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorRef.current) validateWritability(editorRef.current);
|
if (editorRef.current) validateWritability(editorRef.current)
|
||||||
}, [snap.active]);
|
}, [snap.active])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
subscriptionRef?.current?.close();
|
subscriptionRef?.current?.close()
|
||||||
};
|
}
|
||||||
}, []);
|
}, [])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (monacoRef.current) {
|
if (monacoRef.current) {
|
||||||
setMarkers(monacoRef.current);
|
setMarkers(monacoRef.current)
|
||||||
}
|
}
|
||||||
}, [snap.active]);
|
}, [snap.active])
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
saveAllFiles()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const file = snap.files[snap.active]
|
||||||
|
|
||||||
|
const renderNav = () => (
|
||||||
|
<Tabs
|
||||||
|
label="File"
|
||||||
|
activeIndex={snap.active}
|
||||||
|
onChangeActive={idx => (state.active = idx)}
|
||||||
|
extensionRequired
|
||||||
|
onCreateNewTab={createNewFile}
|
||||||
|
onCloseTab={idx => state.files.splice(idx, 1)}
|
||||||
|
onRenameTab={(idx, nwName, oldName = '') => renameFile(oldName, nwName)}
|
||||||
|
headerExtraValidation={{
|
||||||
|
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
|
||||||
|
error: 'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{snap.files.map((file, index) => {
|
||||||
|
return <Tab key={file.name} header={file.name} renameDisabled={!checkWritable(file.name)} />
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
const previewToggle = (
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
if (!isMdPreview) {
|
||||||
|
saveFile(false)
|
||||||
|
}
|
||||||
|
setIsMdPreview(!isMdPreview)
|
||||||
|
}}
|
||||||
|
css={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
m: '$1',
|
||||||
|
fontSize: '$sm'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isMdPreview ? 'Exit Preview' : 'View Preview'}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
width: "100%",
|
width: '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditorNavigation />
|
<EditorNavigation renderNav={renderNav} />
|
||||||
|
{file?.language === 'markdown' && previewToggle}
|
||||||
{snap.files.length > 0 && router.isReady ? (
|
{snap.files.length > 0 && router.isReady ? (
|
||||||
<Editor
|
isMdPreview && file?.language === 'markdown' ? (
|
||||||
className="hooks-editor"
|
<Markdown
|
||||||
keepCurrentModel
|
components={{
|
||||||
defaultLanguage={snap.files?.[snap.active]?.language}
|
a: ({ href, children }) => (
|
||||||
language={snap.files?.[snap.active]?.language}
|
<Link target="_blank" rel="noopener noreferrer" href={href}>
|
||||||
path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
|
{children}
|
||||||
defaultValue={snap.files?.[snap.active]?.content}
|
</Link>
|
||||||
beforeMount={(monaco) => {
|
)
|
||||||
if (!snap.editorCtx) {
|
}}
|
||||||
snap.files.forEach((file) =>
|
>
|
||||||
monaco.editor.createModel(
|
{file.content}
|
||||||
file.content,
|
</Markdown>
|
||||||
file.language,
|
) : (
|
||||||
monaco.Uri.parse(`file:///work/c/${file.name}`)
|
<Monaco
|
||||||
|
keepCurrentModel
|
||||||
|
defaultLanguage={file?.language}
|
||||||
|
language={file?.language}
|
||||||
|
path={`file:///work/c/${file?.name}`}
|
||||||
|
defaultValue={file?.content}
|
||||||
|
// onChange={val => (state.files[snap.active].content = val)} // Auto save?
|
||||||
|
beforeMount={monaco => {
|
||||||
|
// if (!snap.editorCtx) {
|
||||||
|
// snap.files.forEach(file =>
|
||||||
|
// monaco.editor.createModel(
|
||||||
|
// file.content,
|
||||||
|
// file.language,
|
||||||
|
// monaco.Uri.parse(`file:///work/c/${file.name}`)
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// create the web socket
|
||||||
|
if (!subscriptionRef.current) {
|
||||||
|
monaco.languages.register({
|
||||||
|
id: 'c',
|
||||||
|
extensions: ['.c', '.h'],
|
||||||
|
aliases: ['C', 'c', 'H', 'h'],
|
||||||
|
mimetypes: ['text/plain']
|
||||||
|
})
|
||||||
|
monaco.languages.register({
|
||||||
|
id: 'text',
|
||||||
|
extensions: ['.txt'],
|
||||||
|
mimetypes: ['text/plain'],
|
||||||
|
})
|
||||||
|
MonacoServices.install(monaco)
|
||||||
|
const webSocket = createWebSocket(
|
||||||
|
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ''
|
||||||
)
|
)
|
||||||
);
|
subscriptionRef.current = webSocket
|
||||||
}
|
// listen when the web socket is opened
|
||||||
|
listen({
|
||||||
|
webSocket: webSocket as WebSocket,
|
||||||
|
onConnection: connection => {
|
||||||
|
// create and start the language client
|
||||||
|
const languageClient = createLanguageClient(connection)
|
||||||
|
const disposable = languageClient.start()
|
||||||
|
|
||||||
// create the web socket
|
connection.onClose(() => {
|
||||||
if (!subscriptionRef.current) {
|
try {
|
||||||
monaco.languages.register({
|
disposable.dispose()
|
||||||
id: "c",
|
} catch (err) {
|
||||||
extensions: [".c", ".h"],
|
console.log('err', err)
|
||||||
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);
|
|
||||||
languageClient.start();
|
|
||||||
// connection.onDispose((d) => {
|
|
||||||
// console.log("disposed: ", d);
|
|
||||||
// });
|
|
||||||
// connection.onError((ee) => {
|
|
||||||
// console.log(ee =)
|
|
||||||
// })
|
|
||||||
// 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;
|
|
||||||
monacoRef.current = monaco;
|
|
||||||
editor.updateOptions({
|
|
||||||
glyphMargin: true,
|
|
||||||
lightbulb: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
editor.addCommand(
|
|
||||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
|
|
||||||
() => {
|
|
||||||
saveFile();
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
// When the markers (errors/warnings from clangd language server) change
|
// editor.updateOptions({
|
||||||
// Lets improve the markers by adding extra content to them from related
|
// minimap: {
|
||||||
// md files
|
// enabled: false,
|
||||||
monaco.editor.onDidChangeMarkers(() => {
|
// },
|
||||||
if (monacoRef.current) {
|
// ...snap.editorSettings,
|
||||||
setMarkers(monacoRef.current);
|
// });
|
||||||
|
if (!state.editorCtx) {
|
||||||
|
state.editorCtx = ref(monaco.editor)
|
||||||
}
|
}
|
||||||
});
|
}}
|
||||||
|
onMount={(editor, monaco) => {
|
||||||
// Hacky way to hide Peek menu
|
editorRef.current = editor
|
||||||
editor.onContextMenu((e) => {
|
monacoRef.current = monaco
|
||||||
const host =
|
editor.updateOptions({
|
||||||
document.querySelector<HTMLElement>(".shadow-root-host");
|
glyphMargin: true,
|
||||||
|
lightbulb: {
|
||||||
const contextMenuItems =
|
enabled: true
|
||||||
host?.shadowRoot?.querySelectorAll("li.action-item");
|
|
||||||
contextMenuItems?.forEach((k) => {
|
|
||||||
// If menu item contains "Peek" lets hide it
|
|
||||||
if (k.querySelector(".action-label")?.textContent === "Peek") {
|
|
||||||
// @ts-expect-error
|
|
||||||
k["style"].display = "none";
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
||||||
|
saveFile()
|
||||||
|
})
|
||||||
|
// When the markers (errors/warnings from clangd language server) change
|
||||||
|
// Lets improve the markers by adding extra content to them from related
|
||||||
|
// md files
|
||||||
|
monaco.editor.onDidChangeMarkers(() => {
|
||||||
|
if (monacoRef.current) {
|
||||||
|
setMarkers(monacoRef.current)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
validateWritability(editor);
|
// Hacky way to hide Peek menu
|
||||||
}}
|
editor.onContextMenu(e => {
|
||||||
theme={theme === "dark" ? "dark" : "light"}
|
const host = document.querySelector<HTMLElement>('.shadow-root-host')
|
||||||
/>
|
|
||||||
|
const contextMenuItems = host?.shadowRoot?.querySelectorAll('li.action-item')
|
||||||
|
contextMenuItems?.forEach(k => {
|
||||||
|
// If menu item contains "Peek" lets hide it
|
||||||
|
if (k.querySelector('.action-label')?.textContent === 'Peek') {
|
||||||
|
// @ts-expect-error
|
||||||
|
k['style'].display = 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
validateWritability(editor)
|
||||||
|
}}
|
||||||
|
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Container>
|
<Container>
|
||||||
{!snap.loading && router.isReady && (
|
{!snap.loading && router.isReady && (
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
flexDirection: "row",
|
flexDirection: 'row',
|
||||||
width: "$spaces$wide",
|
width: '$spaces$wide',
|
||||||
gap: "$3",
|
gap: '$3',
|
||||||
display: "inline-flex",
|
display: 'inline-flex'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box css={{ display: "inline-flex", pl: "35px" }}>
|
<Box css={{ display: 'inline-flex', pl: '35px' }}>
|
||||||
<ArrowBendLeftUp size={30} />
|
<ArrowBendLeftUp size={30} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box css={{ pl: '0px', pt: '15px', flex: 1, display: 'inline-flex' }}>
|
||||||
css={{ pl: "0px", pt: "15px", flex: 1, display: "inline-flex" }}
|
|
||||||
>
|
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
fontSize: "14px",
|
fontSize: '14px',
|
||||||
maxWidth: "220px",
|
maxWidth: '220px',
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Click the link above to create your file
|
Click the link above to create your file
|
||||||
@@ -274,7 +327,7 @@ const HooksEditor = () => {
|
|||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default HooksEditor;
|
export default HooksEditor
|
||||||
|
|||||||
@@ -1,169 +1,165 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
import * as LabelPrim from '@radix-ui/react-label';
|
import * as LabelPrim from '@radix-ui/react-label'
|
||||||
|
|
||||||
export const Input = styled("input", {
|
export const Input = styled('input', {
|
||||||
// Reset
|
// Reset
|
||||||
appearance: "none",
|
appearance: 'none',
|
||||||
borderWidth: "0",
|
borderWidth: '0',
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box',
|
||||||
fontFamily: "inherit",
|
fontFamily: 'inherit',
|
||||||
outline: "none",
|
outline: 'none',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
flex: "1",
|
flex: '1',
|
||||||
backgroundColor: "$mauve4",
|
backgroundColor: '$mauve4',
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
borderRadius: "$sm",
|
borderRadius: '$sm',
|
||||||
px: "$2",
|
px: '$2',
|
||||||
fontSize: "$md",
|
fontSize: '$md',
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
boxShadow: `0 0 0 1px $colors$mauve8`,
|
boxShadow: `0 0 0 1px $colors$mauve8`,
|
||||||
height: 35,
|
height: 35,
|
||||||
WebkitTapHighlightColor: "rgba(0,0,0,0)",
|
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
|
||||||
"&::before": {
|
'&::before': {
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box'
|
||||||
},
|
},
|
||||||
"&::after": {
|
'&::after': {
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box'
|
||||||
},
|
},
|
||||||
fontVariantNumeric: "tabular-nums",
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
|
||||||
"&:-webkit-autofill": {
|
'&:-webkit-autofill': {
|
||||||
boxShadow: "inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3",
|
boxShadow: 'inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3'
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:-webkit-autofill::first-line": {
|
'&:-webkit-autofill::first-line': {
|
||||||
fontFamily: "$untitled",
|
fontFamily: '$untitled',
|
||||||
color: "$mauve12",
|
color: '$mauve12'
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow: `0 0 0 1px $colors$mauve10`,
|
boxShadow: `0 0 0 1px $colors$mauve10`,
|
||||||
"&:-webkit-autofill": {
|
'&:-webkit-autofill': {
|
||||||
boxShadow: `0 0 0 1px $colors$mauve10`,
|
boxShadow: `0 0 0 1px $colors$mauve10`
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&::placeholder": {
|
'&::placeholder': {
|
||||||
color: "$mauve9",
|
color: '$mauve9'
|
||||||
},
|
},
|
||||||
"&:disabled": {
|
'&:disabled': {
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none',
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
color: "$mauve8",
|
color: '$mauve8',
|
||||||
cursor: "not-allowed",
|
cursor: 'not-allowed',
|
||||||
"&::placeholder": {
|
'&::placeholder': {
|
||||||
color: "$mauve7",
|
color: '$mauve7'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&:read-only": {
|
'&:read-only': {
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
color: "$text",
|
color: '$text',
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow: "inset 0px 0px 0px 1px $colors$mauve7",
|
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve7'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
sm: {
|
sm: {
|
||||||
height: "$5",
|
height: '$5',
|
||||||
fontSize: "$1",
|
fontSize: '$1',
|
||||||
lineHeight: "$sizes$4",
|
lineHeight: '$sizes$4',
|
||||||
"&:-webkit-autofill::first-line": {
|
'&:-webkit-autofill::first-line': {
|
||||||
fontSize: "$1",
|
fontSize: '$1'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
height: "$8",
|
height: '$8',
|
||||||
fontSize: "$1",
|
fontSize: '$1',
|
||||||
lineHeight: "$sizes$5",
|
lineHeight: '$sizes$5',
|
||||||
"&:-webkit-autofill::first-line": {
|
'&:-webkit-autofill::first-line': {
|
||||||
fontSize: "$1",
|
fontSize: '$1'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
height: "$12",
|
height: '$12',
|
||||||
fontSize: "$2",
|
fontSize: '$2',
|
||||||
lineHeight: "$sizes$6",
|
lineHeight: '$sizes$6',
|
||||||
"&:-webkit-autofill::first-line": {
|
'&:-webkit-autofill::first-line': {
|
||||||
fontSize: "$3",
|
fontSize: '$3'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
ghost: {
|
ghost: {
|
||||||
boxShadow: "none",
|
boxShadow: 'none',
|
||||||
backgroundColor: "transparent",
|
backgroundColor: 'transparent',
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
boxShadow: "inset 0 0 0 1px $colors$mauve7",
|
boxShadow: 'inset 0 0 0 1px $colors$mauve7'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
backgroundColor: "$loContrast",
|
backgroundColor: '$loContrast',
|
||||||
boxShadow: `0 0 0 1px $colors$mauve10`,
|
boxShadow: `0 0 0 1px $colors$mauve10`
|
||||||
},
|
},
|
||||||
"&:disabled": {
|
'&:disabled': {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: 'transparent'
|
||||||
},
|
|
||||||
"&:read-only": {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
},
|
||||||
|
'&:read-only': {
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deep: {
|
deep: {
|
||||||
backgroundColor: "$deep",
|
backgroundColor: '$deep',
|
||||||
boxShadow: "none",
|
boxShadow: 'none'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
invalid: {
|
invalid: {
|
||||||
boxShadow: "inset 0 0 0 1px $colors$crimson7",
|
boxShadow: 'inset 0 0 0 1px $colors$crimson7',
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow:
|
boxShadow: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8'
|
||||||
"inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8",
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
valid: {
|
valid: {
|
||||||
boxShadow: "inset 0 0 0 1px $colors$grass7",
|
boxShadow: 'inset 0 0 0 1px $colors$grass7',
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow:
|
boxShadow: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8'
|
||||||
"inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8",
|
}
|
||||||
},
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
cursor: {
|
cursor: {
|
||||||
default: {
|
default: {
|
||||||
cursor: "default",
|
cursor: 'default',
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
cursor: "text",
|
cursor: 'text'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
cursor: "text",
|
cursor: 'text'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
size: "md",
|
size: 'md'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
const ReffedInput = React.forwardRef<
|
const ReffedInput = React.forwardRef<HTMLInputElement, React.ComponentProps<typeof Input>>(
|
||||||
HTMLInputElement,
|
(props, ref) => <Input {...props} ref={ref} />
|
||||||
React.ComponentProps<typeof Input>
|
)
|
||||||
>((props, ref) => <Input {...props} ref={ref} />);
|
|
||||||
|
|
||||||
export default ReffedInput;
|
|
||||||
|
|
||||||
|
export default ReffedInput
|
||||||
|
|
||||||
const LabelRoot = (props: LabelPrim.LabelProps) => <LabelPrim.Root {...props} />
|
const LabelRoot = (props: LabelPrim.LabelProps) => <LabelPrim.Root {...props} />
|
||||||
|
|
||||||
export const Label = styled(LabelRoot, {
|
export const Label = styled(LabelRoot, {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
mb: '$1'
|
mb: '$1'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
const StyledLink = styled("a", {
|
const StyledLink = styled('a', {
|
||||||
color: "CurrentColor",
|
color: 'CurrentColor',
|
||||||
textDecoration: "underline",
|
textDecoration: 'underline',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
variants: {
|
variants: {
|
||||||
highlighted: {
|
highlighted: {
|
||||||
@@ -11,6 +11,6 @@ const StyledLink = styled("a", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default StyledLink;
|
export default StyledLink
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
import {
|
import { useRef, useLayoutEffect, ReactNode, FC, useState } from 'react'
|
||||||
useRef,
|
import { IconProps, Notepad, Prohibit } from 'phosphor-react'
|
||||||
useLayoutEffect,
|
import useStayScrolled from 'react-stay-scrolled'
|
||||||
ReactNode,
|
import NextLink from 'next/link'
|
||||||
FC,
|
|
||||||
useState,
|
|
||||||
useCallback,
|
|
||||||
} from "react";
|
|
||||||
import { Notepad, Prohibit } from "phosphor-react";
|
|
||||||
import useStayScrolled from "react-stay-scrolled";
|
|
||||||
import NextLink from "next/link";
|
|
||||||
|
|
||||||
import Container from "./Container";
|
import Container from './Container'
|
||||||
import LogText from "./LogText";
|
import LogText from './LogText'
|
||||||
import state, { ILog } from "../state";
|
import { ILog } from '../state'
|
||||||
import { Pre, Link, Heading, Button, Text, Flex, Box } from ".";
|
import { Pre, Link, Heading, Button, Text, Flex, Box } from '.'
|
||||||
import regexifyString from "regexify-string";
|
|
||||||
import { useSnapshot } from "valtio";
|
|
||||||
import { AccountDialog } from "./Accounts";
|
|
||||||
|
|
||||||
interface ILogBox {
|
interface ILogBox {
|
||||||
title: string;
|
title: string
|
||||||
clearLog?: () => void;
|
clearLog?: () => void
|
||||||
logs: ILog[];
|
logs: ILog[]
|
||||||
renderNav?: () => ReactNode;
|
renderNav?: () => ReactNode
|
||||||
enhanced?: boolean;
|
enhanced?: boolean
|
||||||
|
Icon?: FC<IconProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogBox: FC<ILogBox> = ({
|
const LogBox: FC<ILogBox> = ({
|
||||||
@@ -33,39 +24,40 @@ const LogBox: FC<ILogBox> = ({
|
|||||||
children,
|
children,
|
||||||
renderNav,
|
renderNav,
|
||||||
enhanced,
|
enhanced,
|
||||||
|
Icon = Notepad
|
||||||
}) => {
|
}) => {
|
||||||
const logRef = useRef<HTMLPreElement>(null);
|
const logRef = useRef<HTMLPreElement>(null)
|
||||||
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
|
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef)
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
stayScrolled();
|
stayScrolled()
|
||||||
}, [stayScrolled, logs]);
|
}, [stayScrolled, logs])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
as="div"
|
as="div"
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
borderTop: "1px solid $mauve6",
|
borderTop: '1px solid $mauve6',
|
||||||
background: "$mauve1",
|
background: '$mauve1',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: "100%",
|
height: '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container
|
<Container
|
||||||
css={{
|
css={{
|
||||||
px: 0,
|
px: 0,
|
||||||
height: "100%",
|
height: '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
fluid
|
fluid
|
||||||
css={{
|
css={{
|
||||||
height: "48px",
|
height: '48px',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
fontSize: "$sm",
|
fontSize: '$sm',
|
||||||
fontWeight: 300,
|
fontWeight: 300
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Heading
|
<Heading
|
||||||
@@ -73,27 +65,27 @@ const LogBox: FC<ILogBox> = ({
|
|||||||
css={{
|
css={{
|
||||||
fontWeight: 300,
|
fontWeight: 300,
|
||||||
m: 0,
|
m: 0,
|
||||||
fontSize: "11px",
|
fontSize: '11px',
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
px: "$3",
|
px: '$3',
|
||||||
textTransform: "uppercase",
|
textTransform: 'uppercase',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
gap: "$3",
|
gap: '$3'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
|
<Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Flex
|
<Flex
|
||||||
row
|
row
|
||||||
align="center"
|
align="center"
|
||||||
css={{
|
// css={{
|
||||||
width: "50%", // TODO make it max without breaking layout!
|
// maxWidth: "100%", // TODO make it max without breaking layout!
|
||||||
}}
|
// }}
|
||||||
>
|
>
|
||||||
{renderNav?.()}
|
{renderNav?.()}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
|
<Flex css={{ ml: 'auto', gap: '$3', marginRight: '$3' }}>
|
||||||
{clearLog && (
|
{clearLog && (
|
||||||
<Button ghost size="xs" onClick={clearLog}>
|
<Button ghost size="xs" onClick={clearLog}>
|
||||||
<Prohibit size="14px" />
|
<Prohibit size="14px" />
|
||||||
@@ -108,17 +100,17 @@ const LogBox: FC<ILogBox> = ({
|
|||||||
css={{
|
css={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
// display: "inline-block",
|
// display: "inline-block",
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
height: "calc(100% - 48px)", // 100% minus the logbox header height
|
height: 'calc(100% - 48px)', // 100% minus the logbox header height
|
||||||
overflowY: "auto",
|
overflowY: 'auto',
|
||||||
fontSize: "13px",
|
fontSize: '13px',
|
||||||
fontWeight: "$body",
|
fontWeight: '$body',
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
px: "$3",
|
px: '$3',
|
||||||
pb: "$2",
|
pb: '$2',
|
||||||
whiteSpace: "normal",
|
whiteSpace: 'normal'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{logs?.map((log, index) => (
|
{logs?.map((log, index) => (
|
||||||
@@ -126,13 +118,13 @@ const LogBox: FC<ILogBox> = ({
|
|||||||
as="span"
|
as="span"
|
||||||
key={log.type + index}
|
key={log.type + index}
|
||||||
css={{
|
css={{
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
|
backgroundColor: enhanced ? '$backgroundAlt' : undefined
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
p: enhanced ? "$1" : undefined,
|
p: enhanced ? '$1' : undefined,
|
||||||
my: enhanced ? "$1" : undefined,
|
my: enhanced ? '$1' : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Log {...log} />
|
<Log {...log} />
|
||||||
@@ -142,76 +134,31 @@ const LogBox: FC<ILogBox> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const Log: FC<ILog> = ({
|
export const Log: FC<ILog> = ({
|
||||||
type,
|
type,
|
||||||
timestring,
|
timestring,
|
||||||
message: _message,
|
message,
|
||||||
link,
|
link,
|
||||||
linkText,
|
linkText,
|
||||||
defaultCollapsed,
|
defaultCollapsed,
|
||||||
jsonData: _jsonData,
|
jsonData
|
||||||
}) => {
|
}) => {
|
||||||
const [expanded, setExpanded] = useState(!defaultCollapsed);
|
const [expanded, setExpanded] = useState(!defaultCollapsed)
|
||||||
const { accounts } = useSnapshot(state);
|
|
||||||
const [dialogAccount, setDialogAccount] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const enrichAccounts = useCallback(
|
|
||||||
(str?: string): ReactNode => {
|
|
||||||
if (!str || !accounts.length) return null;
|
|
||||||
|
|
||||||
const pattern = `(${accounts.map((acc) => acc.address).join("|")})`;
|
|
||||||
const res = regexifyString({
|
|
||||||
pattern: new RegExp(pattern, "gim"),
|
|
||||||
decorator: (match, idx) => {
|
|
||||||
const name = accounts.find((acc) => acc.address === match)?.name;
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={match + idx}
|
|
||||||
as="a"
|
|
||||||
onClick={() => setDialogAccount(match)}
|
|
||||||
title={match}
|
|
||||||
highlighted
|
|
||||||
>
|
|
||||||
{name || match}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
input: str,
|
|
||||||
});
|
|
||||||
|
|
||||||
return <>{res}</>;
|
|
||||||
},
|
|
||||||
[accounts]
|
|
||||||
);
|
|
||||||
|
|
||||||
let message: ReactNode;
|
|
||||||
|
|
||||||
if (typeof _message === 'string') {
|
|
||||||
_message = _message.trim().replace(/\n /gi, "\n");
|
|
||||||
message = enrichAccounts(_message)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = _message
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonData = enrichAccounts(_jsonData);
|
|
||||||
|
|
||||||
|
if (message === undefined) message = <Text muted>{'undefined'}</Text>
|
||||||
|
else if (message === '') message = <Text muted>{'""'}</Text>
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AccountDialog
|
|
||||||
setActiveAccountAddress={setDialogAccount}
|
|
||||||
activeAccountAddress={dialogAccount}
|
|
||||||
/>
|
|
||||||
<LogText variant={type}>
|
<LogText variant={type}>
|
||||||
{timestring && (
|
{timestring && (
|
||||||
<Text muted monospace>
|
<Text muted monospace>
|
||||||
{timestring}{" "}
|
{timestring}{' '}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Pre>{message} </Pre>
|
<Pre>{message}</Pre>
|
||||||
{link && (
|
{link && (
|
||||||
<NextLink href={link} shallow passHref>
|
<NextLink href={link} shallow passHref>
|
||||||
<Link as="a">{linkText}</Link>
|
<Link as="a">{linkText}</Link>
|
||||||
@@ -219,14 +166,13 @@ export const Log: FC<ILog> = ({
|
|||||||
)}
|
)}
|
||||||
{jsonData && (
|
{jsonData && (
|
||||||
<Link onClick={() => setExpanded(!expanded)} as="a">
|
<Link onClick={() => setExpanded(!expanded)} as="a">
|
||||||
{expanded ? "Collapse" : "Expand"}
|
{expanded ? 'Collapse' : 'Expand'}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{expanded && jsonData && <Pre block>{jsonData}</Pre>}
|
{expanded && jsonData && <Pre block>{jsonData}</Pre>}
|
||||||
</LogText>
|
</LogText>
|
||||||
<br />
|
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LogBox;
|
export default LogBox
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
import {
|
|
||||||
useRef,
|
|
||||||
useLayoutEffect,
|
|
||||||
ReactNode,
|
|
||||||
FC,
|
|
||||||
useState,
|
|
||||||
useCallback,
|
|
||||||
} from "react";
|
|
||||||
import { FileJs, Prohibit } from "phosphor-react";
|
|
||||||
import useStayScrolled from "react-stay-scrolled";
|
|
||||||
import NextLink from "next/link";
|
|
||||||
|
|
||||||
import Container from "./Container";
|
|
||||||
import LogText from "./LogText";
|
|
||||||
import state, { ILog } from "../state";
|
|
||||||
import { Pre, Link, Heading, Button, Text, Flex, Box } from ".";
|
|
||||||
import regexifyString from "regexify-string";
|
|
||||||
import { useSnapshot } from "valtio";
|
|
||||||
import { AccountDialog } from "./Accounts";
|
|
||||||
import RunScript from "./RunScript";
|
|
||||||
|
|
||||||
interface ILogBox {
|
|
||||||
title: string;
|
|
||||||
clearLog?: () => void;
|
|
||||||
logs: ILog[];
|
|
||||||
renderNav?: () => ReactNode;
|
|
||||||
enhanced?: boolean;
|
|
||||||
showButtons?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LogBox: FC<ILogBox> = ({
|
|
||||||
title,
|
|
||||||
clearLog,
|
|
||||||
logs,
|
|
||||||
children,
|
|
||||||
renderNav,
|
|
||||||
enhanced,
|
|
||||||
showButtons = true,
|
|
||||||
}) => {
|
|
||||||
const logRef = useRef<HTMLPreElement>(null);
|
|
||||||
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
|
|
||||||
const snap = useSnapshot(state);
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
stayScrolled();
|
|
||||||
}, [stayScrolled, logs]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
as="div"
|
|
||||||
css={{
|
|
||||||
display: "flex",
|
|
||||||
borderTop: "1px solid $mauve6",
|
|
||||||
background: "$mauve1",
|
|
||||||
position: "relative",
|
|
||||||
flex: 1,
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Container
|
|
||||||
css={{
|
|
||||||
px: 0,
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
fluid
|
|
||||||
css={{
|
|
||||||
height: "48px",
|
|
||||||
alignItems: "center",
|
|
||||||
fontSize: "$sm",
|
|
||||||
fontWeight: 300,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Heading
|
|
||||||
as="h3"
|
|
||||||
css={{
|
|
||||||
fontWeight: 300,
|
|
||||||
m: 0,
|
|
||||||
fontSize: "11px",
|
|
||||||
color: "$mauve12",
|
|
||||||
px: "$3",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
alignItems: "center",
|
|
||||||
display: "inline-flex",
|
|
||||||
gap: "$3",
|
|
||||||
mr: "$3",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FileJs size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
|
|
||||||
</Heading>
|
|
||||||
{showButtons && (
|
|
||||||
<Flex css={{ gap: "$3" }}>
|
|
||||||
{snap.files
|
|
||||||
.filter((f) => f.name.endsWith(".js"))
|
|
||||||
.map((file) => (
|
|
||||||
<RunScript file={file} key={file.name} />
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
|
|
||||||
{clearLog && (
|
|
||||||
<Button ghost size="xs" onClick={clearLog}>
|
|
||||||
<Prohibit size="14px" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
<Box
|
|
||||||
as="pre"
|
|
||||||
ref={logRef}
|
|
||||||
css={{
|
|
||||||
margin: 0,
|
|
||||||
// display: "inline-block",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
width: "100%",
|
|
||||||
height: "calc(100% - 48px)", // 100% minus the logbox header height
|
|
||||||
overflowY: "auto",
|
|
||||||
fontSize: "13px",
|
|
||||||
fontWeight: "$body",
|
|
||||||
fontFamily: "$monospace",
|
|
||||||
px: "$3",
|
|
||||||
pb: "$2",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{logs?.map((log, index) => (
|
|
||||||
<Box
|
|
||||||
as="span"
|
|
||||||
key={log.type + index}
|
|
||||||
css={{
|
|
||||||
"@hover": {
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
p: enhanced ? "$1" : undefined,
|
|
||||||
my: enhanced ? "$1" : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Log {...log} />
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Log: FC<ILog> = ({
|
|
||||||
type,
|
|
||||||
timestring,
|
|
||||||
message: _message,
|
|
||||||
link,
|
|
||||||
linkText,
|
|
||||||
defaultCollapsed,
|
|
||||||
jsonData: _jsonData,
|
|
||||||
}) => {
|
|
||||||
const [expanded, setExpanded] = useState(!defaultCollapsed);
|
|
||||||
const { accounts } = useSnapshot(state);
|
|
||||||
const [dialogAccount, setDialogAccount] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const enrichAccounts = useCallback(
|
|
||||||
(str?: string): ReactNode => {
|
|
||||||
if (!str || !accounts.length) return null;
|
|
||||||
|
|
||||||
const pattern = `(${accounts.map((acc) => acc.address).join("|")})`;
|
|
||||||
const res = regexifyString({
|
|
||||||
pattern: new RegExp(pattern, "gim"),
|
|
||||||
decorator: (match, idx) => {
|
|
||||||
const name = accounts.find((acc) => acc.address === match)?.name;
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={match + idx}
|
|
||||||
as="a"
|
|
||||||
onClick={() => setDialogAccount(match)}
|
|
||||||
title={match}
|
|
||||||
highlighted
|
|
||||||
>
|
|
||||||
{name || match}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
input: str,
|
|
||||||
});
|
|
||||||
|
|
||||||
return <>{res}</>;
|
|
||||||
},
|
|
||||||
[accounts]
|
|
||||||
);
|
|
||||||
|
|
||||||
let message: ReactNode;
|
|
||||||
|
|
||||||
if (typeof _message === "string") {
|
|
||||||
_message = _message.trim().replace(/\n /gi, "\n");
|
|
||||||
message = enrichAccounts(_message);
|
|
||||||
} else {
|
|
||||||
message = _message;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonData = enrichAccounts(_jsonData);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AccountDialog
|
|
||||||
setActiveAccountAddress={setDialogAccount}
|
|
||||||
activeAccountAddress={dialogAccount}
|
|
||||||
/>
|
|
||||||
<LogText variant={type}>
|
|
||||||
{timestring && (
|
|
||||||
<Text muted monospace>
|
|
||||||
{timestring}{" "}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<Pre>{message} </Pre>
|
|
||||||
{link && (
|
|
||||||
<NextLink href={link} shallow passHref>
|
|
||||||
<Link as="a">{linkText}</Link>
|
|
||||||
</NextLink>
|
|
||||||
)}
|
|
||||||
{jsonData && (
|
|
||||||
<Link onClick={() => setExpanded(!expanded)} as="a">
|
|
||||||
{expanded ? "Collapse" : "Expand"}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{expanded && jsonData && <Pre block>{jsonData}</Pre>}
|
|
||||||
</LogText>
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LogBox;
|
|
||||||
@@ -1,31 +1,31 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
const Text = styled("span", {
|
const Text = styled('span', {
|
||||||
fontFamily: "$monospace",
|
fontFamily: '$monospace',
|
||||||
lineHeight: "$body",
|
lineHeight: '$body',
|
||||||
color: "$text",
|
color: '$text',
|
||||||
wordWrap: "break-word",
|
wordWrap: 'break-word',
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
log: {
|
log: {
|
||||||
color: "$text",
|
color: '$text'
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
color: "$warning",
|
color: '$warning'
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
color: "$error",
|
color: '$error'
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
color: "$success",
|
color: '$success'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
capitalize: {
|
capitalize: {
|
||||||
true: {
|
true: {
|
||||||
textTransform: "capitalize",
|
textTransform: 'capitalize'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default Text;
|
export default Text
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
const SVG = styled("svg", {
|
const SVG = styled('svg', {
|
||||||
"& #path": {
|
'& #path': {
|
||||||
fill: "$accent",
|
fill: '$accent'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
function Logo({
|
function Logo({ width, height }: { width?: string | number; height?: string | number }) {
|
||||||
width,
|
|
||||||
height,
|
|
||||||
}: {
|
|
||||||
width?: string | number;
|
|
||||||
height?: string | number;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<SVG
|
<SVG
|
||||||
width={width || "1.1em"}
|
width={width || '1.1em'}
|
||||||
height={height || "1.1em"}
|
height={height || '1.1em'}
|
||||||
viewBox="0 0 294 283"
|
viewBox="0 0 294 283"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -28,7 +22,7 @@ function Logo({
|
|||||||
fill="#9D2DFF"
|
fill="#9D2DFF"
|
||||||
/>
|
/>
|
||||||
</SVG>
|
</SVG>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Logo;
|
export default Logo
|
||||||
|
|||||||
14
components/Markdown.tsx
Normal file
14
components/Markdown.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
|
const Markdown = styled(ReactMarkdown, {
|
||||||
|
px: '$8',
|
||||||
|
'@md': {
|
||||||
|
px: '$20'
|
||||||
|
},
|
||||||
|
pb: '$5',
|
||||||
|
height: '100%',
|
||||||
|
overflowY: 'auto'
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Markdown
|
||||||
71
components/Monaco.tsx
Normal file
71
components/Monaco.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import Editor, { loader, EditorProps, Monaco } from '@monaco-editor/react'
|
||||||
|
import { CSS } from '@stitches/react'
|
||||||
|
import type monaco from 'monaco-editor'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
import { FC, MutableRefObject, ReactNode } from 'react'
|
||||||
|
import { Flex } from '.'
|
||||||
|
import dark from '../theme/editor/amy.json'
|
||||||
|
import light from '../theme/editor/xcode_default.json'
|
||||||
|
|
||||||
|
export type MonacoProps = EditorProps & {
|
||||||
|
id?: string
|
||||||
|
rootProps?: { css: CSS } & Record<string, any>
|
||||||
|
overlay?: ReactNode
|
||||||
|
editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor>
|
||||||
|
monacoRef?: MutableRefObject<typeof monaco>
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.config({
|
||||||
|
paths: {
|
||||||
|
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Monaco: FC<MonacoProps> = ({
|
||||||
|
id,
|
||||||
|
path = `file:///${id}`,
|
||||||
|
className = id,
|
||||||
|
language = 'json',
|
||||||
|
overlay,
|
||||||
|
editorRef,
|
||||||
|
monacoRef,
|
||||||
|
beforeMount,
|
||||||
|
rootProps,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const setTheme = (monaco: Monaco) => {
|
||||||
|
monaco.editor.defineTheme('dark', dark as any)
|
||||||
|
monaco.editor.defineTheme('light', light as any)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
fluid
|
||||||
|
column
|
||||||
|
{...rootProps}
|
||||||
|
css={{
|
||||||
|
position: 'relative',
|
||||||
|
height: '100%',
|
||||||
|
...rootProps?.css
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
className={className}
|
||||||
|
language={language}
|
||||||
|
path={path}
|
||||||
|
beforeMount={monaco => {
|
||||||
|
beforeMount?.(monaco)
|
||||||
|
|
||||||
|
setTheme(monaco)
|
||||||
|
}}
|
||||||
|
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{overlay && (
|
||||||
|
<Flex css={{ position: 'absolute', bottom: 0, right: 0, width: '100%' }}>{overlay}</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Monaco
|
||||||
@@ -1,96 +1,90 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
|
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from 'valtio'
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from 'next/router'
|
||||||
import { FolderOpen, X, ArrowUpRight, BookOpen } from "phosphor-react";
|
import { FolderOpen, X, ArrowUpRight, BookOpen } from 'phosphor-react'
|
||||||
|
|
||||||
import Stack from "./Stack";
|
import Stack from './Stack'
|
||||||
import Logo from "./Logo";
|
import Logo from './Logo'
|
||||||
import Button from "./Button";
|
import Button from './Button'
|
||||||
import Flex from "./Flex";
|
import Flex from './Flex'
|
||||||
import Container from "./Container";
|
import Container from './Container'
|
||||||
import Box from "./Box";
|
import Box from './Box'
|
||||||
import ThemeChanger from "./ThemeChanger";
|
import ThemeChanger from './ThemeChanger'
|
||||||
import state from "../state";
|
import state from '../state'
|
||||||
import Heading from "./Heading";
|
import Heading from './Heading'
|
||||||
import Text from "./Text";
|
import Text from './Text'
|
||||||
import Spinner from "./Spinner";
|
import Spinner from './Spinner'
|
||||||
import truncate from "../utils/truncate";
|
import truncate from '../utils/truncate'
|
||||||
import ButtonGroup from "./ButtonGroup";
|
import ButtonGroup from './ButtonGroup'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger
|
||||||
} from "./Dialog";
|
} from './Dialog'
|
||||||
import PanelBox from "./PanelBox";
|
import PanelBox from './PanelBox'
|
||||||
import { templateFileIds } from "../state/constants";
|
import { templateFileIds } from '../state/constants'
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
import Starter from "../components/icons/Starter";
|
|
||||||
import Firewall from "../components/icons/Firewall";
|
|
||||||
import Notary from "../components/icons/Notary";
|
|
||||||
import Carbon from "../components/icons/Carbon";
|
|
||||||
import Peggy from "../components/icons/Peggy";
|
|
||||||
|
|
||||||
const ImageWrapper = styled(Flex, {
|
const ImageWrapper = styled(Flex, {
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
mt: "$2",
|
mt: '$2',
|
||||||
mb: "$10",
|
mb: '$10',
|
||||||
svg: {
|
svg: {
|
||||||
// fill: "red",
|
// fill: "red",
|
||||||
".angle": {
|
'.angle': {
|
||||||
fill: "$text",
|
fill: '$text'
|
||||||
},
|
},
|
||||||
":not(.angle)": {
|
':not(.angle)': {
|
||||||
stroke: "$text",
|
stroke: '$text'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const Navigation = () => {
|
const Navigation = () => {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state)
|
||||||
const slug = router.query?.slug;
|
const slug = router.query?.slug
|
||||||
const gistId = Array.isArray(slug) ? slug[0] : null;
|
const gistId = Array.isArray(slug) ? slug[0] : null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as="nav"
|
as="nav"
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
backgroundColor: "$mauve1",
|
backgroundColor: '$mauve1',
|
||||||
borderBottom: "1px solid $mauve6",
|
borderBottom: '1px solid $mauve6',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
zIndex: 2003,
|
zIndex: 2003,
|
||||||
height: "60px",
|
height: '60px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container
|
<Container
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
borderRight: "1px solid $colors$mauve6",
|
borderRight: '1px solid $colors$mauve6',
|
||||||
py: "$3",
|
py: '$3',
|
||||||
pr: "$4",
|
pr: '$4'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link href={gistId ? `/develop/${gistId}` : "/develop"} passHref>
|
<Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref>
|
||||||
<Box
|
<Box
|
||||||
as="a"
|
as="a"
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
color: "$textColor",
|
color: '$textColor'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Logo width="32px" height="32px" />
|
<Logo width="32px" height="32px" />
|
||||||
@@ -98,38 +92,30 @@ const Navigation = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
ml: "$5",
|
ml: '$5',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
gap: "1px",
|
gap: '1px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{snap.loading ? (
|
{snap.loading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Heading css={{ lineHeight: 1 }}>
|
<Heading css={{ lineHeight: 1 }}>{snap.gistName || 'XRPL Hooks'}</Heading>
|
||||||
{snap.files?.[0]?.name || "XRPL Hooks"}
|
<Text css={{ fontSize: '$xs', color: '$mauve10', lineHeight: 1 }}>
|
||||||
</Heading>
|
{snap.files.length > 0 ? 'Gist: ' : 'Builder'}
|
||||||
<Text
|
|
||||||
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
|
|
||||||
>
|
|
||||||
{snap.files.length > 0 ? "Gist: " : "Builder"}
|
|
||||||
{snap.files.length > 0 && (
|
{snap.files.length > 0 && (
|
||||||
<Link
|
<Link
|
||||||
href={`https://gist.github.com/${snap.gistOwner || ""}/${
|
href={`https://gist.github.com/${snap.gistOwner || ''}/${snap.gistId || ''}`}
|
||||||
snap.gistId || ""
|
|
||||||
}`}
|
|
||||||
passHref
|
passHref
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
as="a"
|
as="a"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
css={{ color: "$mauve12" }}
|
css={{ color: '$mauve12' }}
|
||||||
>
|
>
|
||||||
{`${snap.gistOwner || "-"}/${truncate(
|
{`${snap.gistOwner || '-'}/${truncate(snap.gistId || '')}`}
|
||||||
snap.gistId || ""
|
|
||||||
)}`}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@@ -138,11 +124,8 @@ const Navigation = () => {
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{router.isReady && (
|
{router.isReady && (
|
||||||
<ButtonGroup css={{ marginLeft: "auto" }}>
|
<ButtonGroup css={{ marginLeft: 'auto' }}>
|
||||||
<Dialog
|
<Dialog open={snap.mainModalOpen} onOpenChange={open => (state.mainModalOpen = open)}>
|
||||||
open={snap.mainModalOpen}
|
|
||||||
onOpenChange={(open) => (state.mainModalOpen = open)}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button outline>
|
<Button outline>
|
||||||
<FolderOpen size="15px" />
|
<FolderOpen size="15px" />
|
||||||
@@ -150,51 +133,51 @@ const Navigation = () => {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
maxWidth: "1080px",
|
maxWidth: '1080px',
|
||||||
width: "80vw",
|
width: '80vw',
|
||||||
maxHeight: "80%",
|
maxHeight: '80%',
|
||||||
backgroundColor: "$mauve1 !important",
|
backgroundColor: '$mauve1 !important',
|
||||||
overflowY: "auto",
|
overflowY: 'auto',
|
||||||
background: "black",
|
background: 'black',
|
||||||
p: 0,
|
p: 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
height: "100%",
|
height: '100%',
|
||||||
"@md": {
|
'@md': {
|
||||||
flexDirection: "row",
|
flexDirection: 'row',
|
||||||
height: "100%",
|
height: '100%'
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
borderBottom: "1px solid $colors$mauve5",
|
borderBottom: '1px solid $colors$mauve5',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
minWidth: "240px",
|
minWidth: '240px',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
p: "$7",
|
p: '$7',
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
"@md": {
|
'@md': {
|
||||||
width: "30%",
|
width: '30%',
|
||||||
maxWidth: "300px",
|
maxWidth: '300px',
|
||||||
borderBottom: "0px",
|
borderBottom: '0px',
|
||||||
borderRight: "1px solid $colors$mauve5",
|
borderRight: '1px solid $colors$mauve5'
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle
|
<DialogTitle
|
||||||
css={{
|
css={{
|
||||||
textTransform: "uppercase",
|
textTransform: 'uppercase',
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
gap: "$3",
|
gap: '$3',
|
||||||
fontSize: "$xl",
|
fontSize: '$xl',
|
||||||
lineHeight: "$one",
|
lineHeight: '$one',
|
||||||
fontWeight: "$bold",
|
fontWeight: '$bold'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Logo width="48px" height="48px" /> XRPL Hooks Builder
|
<Logo width="48px" height="48px" /> XRPL Hooks Builder
|
||||||
@@ -202,30 +185,27 @@ const Navigation = () => {
|
|||||||
<DialogDescription as="div">
|
<DialogDescription as="div">
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
color: "inherit",
|
color: 'inherit',
|
||||||
my: "$5",
|
my: '$5',
|
||||||
mb: "$7",
|
mb: '$7'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Hooks add smart contract functionality to the XRP
|
Hooks add smart contract functionality to the XRP Ledger.
|
||||||
Ledger.
|
|
||||||
</Text>
|
</Text>
|
||||||
<Flex
|
<Flex css={{ flexDirection: 'column', gap: '$2', mt: '$2' }}>
|
||||||
css={{ flexDirection: "column", gap: "$2", mt: "$2" }}
|
|
||||||
>
|
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
gap: "$3",
|
gap: '$3',
|
||||||
color: "$purple11",
|
color: '$purple11',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
color: "$purple12",
|
color: '$purple12'
|
||||||
},
|
|
||||||
"&:focus": {
|
|
||||||
outline: 0,
|
|
||||||
},
|
},
|
||||||
|
'&:focus': {
|
||||||
|
outline: 0
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
as="a"
|
as="a"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
@@ -237,16 +217,16 @@ const Navigation = () => {
|
|||||||
|
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
gap: "$3",
|
gap: '$3',
|
||||||
color: "$purple11",
|
color: '$purple11',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
color: "$purple12",
|
color: '$purple12'
|
||||||
},
|
|
||||||
"&:focus": {
|
|
||||||
outline: 0,
|
|
||||||
},
|
},
|
||||||
|
'&:focus': {
|
||||||
|
outline: 0
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
as="a"
|
as="a"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
@@ -257,16 +237,16 @@ const Navigation = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
css={{
|
css={{
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
gap: "$3",
|
gap: '$3',
|
||||||
color: "$purple11",
|
color: '$purple11',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
color: "$purple12",
|
color: '$purple12'
|
||||||
},
|
|
||||||
"&:focus": {
|
|
||||||
outline: 0,
|
|
||||||
},
|
},
|
||||||
|
'&:focus': {
|
||||||
|
outline: 0
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
as="a"
|
as="a"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
@@ -281,99 +261,47 @@ const Navigation = () => {
|
|||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
display: "grid",
|
display: 'grid',
|
||||||
gridTemplateColumns: "1fr",
|
gridTemplateColumns: '1fr',
|
||||||
gridTemplateRows: "max-content",
|
gridTemplateRows: 'max-content',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
p: "$7",
|
p: '$7',
|
||||||
pb: "$16",
|
pb: '$16',
|
||||||
gap: "$3",
|
gap: '$3',
|
||||||
alignItems: "normal",
|
alignItems: 'normal',
|
||||||
flexWrap: "wrap",
|
flexWrap: 'wrap',
|
||||||
backgroundColor: "$mauve1",
|
backgroundColor: '$mauve1',
|
||||||
"@md": {
|
'@md': {
|
||||||
gridTemplateColumns: "1fr 1fr",
|
gridTemplateColumns: '1fr 1fr',
|
||||||
gridTemplateRows: "max-content",
|
gridTemplateRows: 'max-content'
|
||||||
},
|
|
||||||
"@lg": {
|
|
||||||
gridTemplateColumns: "1fr 1fr 1fr",
|
|
||||||
gridTemplateRows: "max-content",
|
|
||||||
},
|
},
|
||||||
|
'@lg': {
|
||||||
|
gridTemplateColumns: '1fr 1fr 1fr',
|
||||||
|
gridTemplateRows: 'max-content'
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PanelBox
|
{Object.values(templateFileIds).map(template => (
|
||||||
as="a"
|
<PanelBox key={template.id} as="a" href={`/develop/${template.id}`}>
|
||||||
href={`/develop/${templateFileIds.starter}`}
|
<ImageWrapper>{template.icon()}</ImageWrapper>
|
||||||
>
|
<Heading>{template.name}</Heading>
|
||||||
<ImageWrapper>
|
|
||||||
<Starter />
|
|
||||||
</ImageWrapper>
|
|
||||||
<Heading>Starter</Heading>
|
|
||||||
|
|
||||||
<Text>
|
<Text>{template.description}</Text>
|
||||||
Just a basic starter with essential imports, just
|
</PanelBox>
|
||||||
accepts any transaction coming through
|
))}
|
||||||
</Text>
|
|
||||||
</PanelBox>
|
|
||||||
|
|
||||||
<PanelBox
|
|
||||||
as="a"
|
|
||||||
href={`/develop/${templateFileIds.firewall}`}
|
|
||||||
css={{ alignItems: "flex-start" }}
|
|
||||||
>
|
|
||||||
<ImageWrapper>
|
|
||||||
<Firewall />
|
|
||||||
</ImageWrapper>
|
|
||||||
<Heading>Firewall</Heading>
|
|
||||||
<Text>
|
|
||||||
This Hook essentially checks a blacklist of accounts
|
|
||||||
</Text>
|
|
||||||
</PanelBox>
|
|
||||||
<PanelBox
|
|
||||||
as="a"
|
|
||||||
href={`/develop/${templateFileIds.notary}`}
|
|
||||||
>
|
|
||||||
<ImageWrapper>
|
|
||||||
<Notary />
|
|
||||||
</ImageWrapper>
|
|
||||||
<Heading>Notary</Heading>
|
|
||||||
<Text>
|
|
||||||
Collecting signatures for multi-sign transactions
|
|
||||||
</Text>
|
|
||||||
</PanelBox>
|
|
||||||
<PanelBox
|
|
||||||
as="a"
|
|
||||||
href={`/develop/${templateFileIds.carbon}`}
|
|
||||||
>
|
|
||||||
<ImageWrapper>
|
|
||||||
<Carbon />
|
|
||||||
</ImageWrapper>
|
|
||||||
<Heading>Carbon</Heading>
|
|
||||||
<Text>Send a percentage of sum to an address</Text>
|
|
||||||
</PanelBox>
|
|
||||||
<PanelBox
|
|
||||||
as="a"
|
|
||||||
href={`/develop/${templateFileIds.peggy}`}
|
|
||||||
>
|
|
||||||
<ImageWrapper>
|
|
||||||
<Peggy />
|
|
||||||
</ImageWrapper>
|
|
||||||
<Heading>Peggy</Heading>
|
|
||||||
<Text>An oracle based stable coin hook</Text>
|
|
||||||
</PanelBox>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: "$1",
|
top: '$1',
|
||||||
right: "$1",
|
right: '$1',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
background: "$mauve1",
|
background: '$mauve1',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
borderRadius: "$full",
|
borderRadius: '$full',
|
||||||
p: "$1",
|
p: '$1'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X size="20px" />
|
<X size="20px" />
|
||||||
@@ -387,61 +315,39 @@ const Navigation = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
flexWrap: "nowrap",
|
flexWrap: 'nowrap',
|
||||||
marginLeft: "$4",
|
marginLeft: '$4',
|
||||||
overflowX: "scroll",
|
overflowX: 'scroll',
|
||||||
"&::-webkit-scrollbar": {
|
'&::-webkit-scrollbar': {
|
||||||
height: 0,
|
height: 0,
|
||||||
background: "transparent",
|
background: 'transparent'
|
||||||
},
|
},
|
||||||
|
scrollbarColor: 'transparent',
|
||||||
|
scrollbarWidth: 'none'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
css={{
|
css={{
|
||||||
ml: "$4",
|
ml: '$4',
|
||||||
gap: "$3",
|
gap: '$3',
|
||||||
flexWrap: "nowrap",
|
flexWrap: 'nowrap',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
marginLeft: "auto",
|
marginLeft: 'auto'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Link
|
<Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref shallow>
|
||||||
href={gistId ? `/develop/${gistId}` : "/develop"}
|
<Button as="a" outline={!router.pathname.includes('/develop')} uppercase>
|
||||||
passHref
|
|
||||||
shallow
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
outline={!router.pathname.includes("/develop")}
|
|
||||||
uppercase
|
|
||||||
>
|
|
||||||
Develop
|
Develop
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link href={gistId ? `/deploy/${gistId}` : '/deploy'} passHref shallow>
|
||||||
href={gistId ? `/deploy/${gistId}` : "/deploy"}
|
<Button as="a" outline={!router.pathname.includes('/deploy')} uppercase>
|
||||||
passHref
|
|
||||||
shallow
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
outline={!router.pathname.includes("/deploy")}
|
|
||||||
uppercase
|
|
||||||
>
|
|
||||||
Deploy
|
Deploy
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link href={gistId ? `/test/${gistId}` : '/test'} passHref shallow>
|
||||||
href={gistId ? `/test/${gistId}` : "/test"}
|
<Button as="a" outline={!router.pathname.includes('/test')} uppercase>
|
||||||
passHref
|
|
||||||
shallow
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
outline={!router.pathname.includes("/test")}
|
|
||||||
uppercase
|
|
||||||
>
|
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -457,7 +363,7 @@ const Navigation = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Navigation;
|
export default Navigation
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
import Heading from "./Heading";
|
import Heading from './Heading'
|
||||||
import Text from "./Text";
|
import Text from './Text'
|
||||||
|
|
||||||
const PanelBox = styled("div", {
|
const PanelBox = styled('div', {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
border: "1px solid $colors$mauve6",
|
border: '1px solid $colors$mauve6',
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
padding: "$3",
|
padding: '$3',
|
||||||
borderRadius: "$sm",
|
borderRadius: '$sm',
|
||||||
fontWeight: "lighter",
|
fontWeight: 'lighter',
|
||||||
height: "auto",
|
height: 'auto',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
flex: "1 1 0px",
|
flex: '1 1 0px',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
border: "1px solid $colors$mauve9",
|
border: '1px solid $colors$mauve9'
|
||||||
},
|
},
|
||||||
[`& ${Heading}`]: {
|
[`& ${Heading}`]: {
|
||||||
fontWeight: "lighter",
|
fontWeight: 'lighter',
|
||||||
mb: "$2",
|
mb: '$2'
|
||||||
},
|
},
|
||||||
[`& ${Text}`]: {
|
[`& ${Text}`]: {
|
||||||
fontWeight: "lighter",
|
fontWeight: 'lighter',
|
||||||
color: "$mauve10",
|
color: '$mauve10',
|
||||||
fontSize: "$sm",
|
fontSize: '$sm'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default PanelBox;
|
export default PanelBox
|
||||||
|
|||||||
@@ -1,92 +1,89 @@
|
|||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from 'react'
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||||
import { styled, keyframes } from "../stitches.config";
|
import { styled, keyframes } from '../stitches.config'
|
||||||
|
|
||||||
const slideUpAndFade = keyframes({
|
const slideUpAndFade = keyframes({
|
||||||
"0%": { opacity: 0, transform: "translateY(2px)" },
|
'0%': { opacity: 0, transform: 'translateY(2px)' },
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
'100%': { opacity: 1, transform: 'translateY(0)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const slideRightAndFade = keyframes({
|
const slideRightAndFade = keyframes({
|
||||||
"0%": { opacity: 0, transform: "translateX(-2px)" },
|
'0%': { opacity: 0, transform: 'translateX(-2px)' },
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
'100%': { opacity: 1, transform: 'translateX(0)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const slideDownAndFade = keyframes({
|
const slideDownAndFade = keyframes({
|
||||||
"0%": { opacity: 0, transform: "translateY(-2px)" },
|
'0%': { opacity: 0, transform: 'translateY(-2px)' },
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
'100%': { opacity: 1, transform: 'translateY(0)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const slideLeftAndFade = keyframes({
|
const slideLeftAndFade = keyframes({
|
||||||
"0%": { opacity: 0, transform: "translateX(2px)" },
|
'0%': { opacity: 0, transform: 'translateX(2px)' },
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
'100%': { opacity: 1, transform: 'translateX(0)' }
|
||||||
});
|
})
|
||||||
const StyledContent = styled(PopoverPrimitive.Content, {
|
const StyledContent = styled(PopoverPrimitive.Content, {
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
padding: "$3 $3",
|
padding: '$3 $3',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
color: "$text",
|
color: '$text',
|
||||||
backgroundColor: "$background",
|
backgroundColor: '$background',
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
|
'0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
|
||||||
"@media (prefers-reduced-motion: no-preference)": {
|
'@media (prefers-reduced-motion: no-preference)': {
|
||||||
animationDuration: "400ms",
|
animationDuration: '400ms',
|
||||||
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
|
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
willChange: "transform, opacity",
|
willChange: 'transform, opacity',
|
||||||
'&[data-state="open"]': {
|
'&[data-state="open"]': {
|
||||||
'&[data-side="top"]': { animationName: slideDownAndFade },
|
'&[data-side="top"]': { animationName: slideDownAndFade },
|
||||||
'&[data-side="right"]': { animationName: slideLeftAndFade },
|
'&[data-side="right"]': { animationName: slideLeftAndFade },
|
||||||
'&[data-side="bottom"]': { animationName: slideUpAndFade },
|
'&[data-side="bottom"]': { animationName: slideUpAndFade },
|
||||||
'&[data-side="left"]': { animationName: slideRightAndFade },
|
'&[data-side="left"]': { animationName: slideRightAndFade }
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
backgroundColor: "$mauve5",
|
backgroundColor: '$mauve5',
|
||||||
boxShadow:
|
boxShadow: '0px 5px 38px -2px rgba(22, 23, 24, 1), 0px 10px 20px 0px rgba(22, 23, 24, 1)'
|
||||||
"0px 5px 38px -2px rgba(22, 23, 24, 1), 0px 10px 20px 0px rgba(22, 23, 24, 1)",
|
}
|
||||||
},
|
})
|
||||||
});
|
|
||||||
|
|
||||||
const StyledArrow = styled(PopoverPrimitive.Arrow, {
|
const StyledArrow = styled(PopoverPrimitive.Arrow, {
|
||||||
fill: "$colors$mauve2",
|
fill: '$colors$mauve2',
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
fill: "$mauve5",
|
fill: '$mauve5'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledClose = styled(PopoverPrimitive.Close, {
|
const StyledClose = styled(PopoverPrimitive.Close, {
|
||||||
all: "unset",
|
all: 'unset',
|
||||||
fontFamily: "inherit",
|
fontFamily: 'inherit',
|
||||||
borderRadius: "100%",
|
borderRadius: '100%',
|
||||||
height: 25,
|
height: 25,
|
||||||
width: 25,
|
width: 25,
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
color: "$text",
|
color: '$text',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 5,
|
top: 5,
|
||||||
right: 5,
|
right: 5
|
||||||
});
|
})
|
||||||
|
|
||||||
// Exports
|
// Exports
|
||||||
export const PopoverRoot = PopoverPrimitive.Root;
|
export const PopoverRoot = PopoverPrimitive.Root
|
||||||
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
export const PopoverTrigger = PopoverPrimitive.Trigger
|
||||||
export const PopoverContent = StyledContent;
|
export const PopoverContent = StyledContent
|
||||||
export const PopoverArrow = StyledArrow;
|
export const PopoverArrow = StyledArrow
|
||||||
export const PopoverClose = StyledClose;
|
export const PopoverClose = StyledClose
|
||||||
|
|
||||||
interface IPopover {
|
interface IPopover {
|
||||||
content: string | ReactNode;
|
content: string | ReactNode
|
||||||
open?: boolean;
|
open?: boolean
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Popover: React.FC<
|
const Popover: React.FC<IPopover & React.ComponentProps<typeof PopoverContent>> = ({
|
||||||
IPopover & React.ComponentProps<typeof PopoverContent>
|
|
||||||
> = ({
|
|
||||||
children,
|
children,
|
||||||
content,
|
content,
|
||||||
open,
|
open,
|
||||||
@@ -94,16 +91,12 @@ const Popover: React.FC<
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
...rest
|
...rest
|
||||||
}) => (
|
}) => (
|
||||||
<PopoverRoot
|
<PopoverRoot open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
|
||||||
open={open}
|
|
||||||
defaultOpen={defaultOpen}
|
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||||
<PopoverContent sideOffset={5} {...rest}>
|
<PopoverContent sideOffset={5} {...rest}>
|
||||||
{content} <PopoverArrow offset={5} className="arrow" />
|
{content} <PopoverArrow offset={5} className="arrow" />
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</PopoverRoot>
|
</PopoverRoot>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default Popover;
|
export default Popover
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
const Pre = styled("span", {
|
const Pre = styled('span', {
|
||||||
m: 0,
|
m: 0,
|
||||||
wordBreak: "break-all",
|
wordBreak: 'break-all',
|
||||||
fontFamily: '$monospace',
|
fontFamily: '$monospace',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
variants: {
|
variants: {
|
||||||
fluid: {
|
fluid: {
|
||||||
true: {
|
true: {
|
||||||
width: "100%",
|
width: '100%'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
line: {
|
line: {
|
||||||
true: {
|
true: {
|
||||||
@@ -21,7 +21,7 @@ const Pre = styled("span", {
|
|||||||
display: 'block'
|
display: 'block'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default Pre;
|
export default Pre
|
||||||
|
|||||||
24
components/ResultLink.tsx
Normal file
24
components/ResultLink.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { Link } from '.'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
result?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResultLink: FC<Props> = ({ result }) => {
|
||||||
|
if (!result) return null
|
||||||
|
let href: string
|
||||||
|
if (result === 'tesSUCCESS') {
|
||||||
|
href = 'https://xrpl.org/tes-success.html'
|
||||||
|
} else {
|
||||||
|
// Going shortcut here because of url structure, if that changes we will do it manually
|
||||||
|
href = `https://xrpl.org/${result.slice(0, 3)}-codes.html`
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link as="a" href={href} target="_blank" rel="noopener noreferrer">
|
||||||
|
{result}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResultLink
|
||||||
@@ -1,32 +1,38 @@
|
|||||||
import * as Handlebars from "handlebars";
|
import { Play, X } from 'phosphor-react'
|
||||||
import { Play, X } from "phosphor-react";
|
import { HTMLInputTypeAttribute, useCallback, useEffect, useState } from 'react'
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import state, { IAccount, IFile, ILog } from '../../state'
|
||||||
import state, { IFile, ILog } from "../../state";
|
import Button from '../Button'
|
||||||
import Button from "../Button";
|
import Box from '../Box'
|
||||||
import Box from "../Box";
|
import Input, { Label } from '../Input'
|
||||||
import Input from "../Input";
|
import Stack from '../Stack'
|
||||||
import Stack from "../Stack";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogClose,
|
DialogClose
|
||||||
} from "../Dialog";
|
} from '../Dialog'
|
||||||
import Flex from "../Flex";
|
import Flex from '../Flex'
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from 'valtio'
|
||||||
import Select from "../Select";
|
import Select from '../Select'
|
||||||
import { saveFile } from "../../state/actions/saveFile";
|
import Text from '../Text'
|
||||||
|
import { saveFile } from '../../state/actions/saveFile'
|
||||||
|
import { getErrors, getTags } from '../../utils/comment-parser'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
Handlebars.registerHelper(
|
const generateHtmlTemplate = async (code: string, data?: Record<string, any>) => {
|
||||||
"customize_input",
|
let processString: string | undefined
|
||||||
function (/* dynamic arguments */) {
|
const process = { env: { NODE_ENV: 'production' } } as any
|
||||||
return new Handlebars.SafeString(arguments[0]);
|
if (data) {
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
process.env[key] = data[key]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const generateHtmlTemplate = (code: string) => {
|
processString = JSON.stringify(process)
|
||||||
|
|
||||||
|
const libs = (await import("xrpl-accountlib/dist/browser.hook-bundle.js")).default;
|
||||||
return `
|
return `
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -55,124 +61,147 @@ const generateHtmlTemplate = (code: string) => {
|
|||||||
parent.window.postMessage({ type: 'warning', args: args || [] }, '*');
|
parent.window.postMessage({ type: 'warning', args: args || [] }, '*');
|
||||||
warnLog.apply(console, args);
|
warnLog.apply(console, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var process = '${processString || '{}'}';
|
||||||
|
process = JSON.parse(process);
|
||||||
|
window.process = process
|
||||||
|
|
||||||
|
function windowErrorHandler(event) {
|
||||||
|
event.preventDefault() // to prevent automatically logging to console
|
||||||
|
console.error(event.error?.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('error', windowErrorHandler);
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script>
|
||||||
|
${libs}
|
||||||
|
</script>
|
||||||
|
<script type="module">
|
||||||
${code}
|
${code}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`
|
||||||
};
|
}
|
||||||
|
|
||||||
type Fields = Record<
|
type Fields = Record<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
key: string;
|
name: string
|
||||||
value: string;
|
value: string
|
||||||
label?: string;
|
type?: 'Account' | `Account.${keyof IAccount}` | HTMLInputTypeAttribute
|
||||||
type?: string;
|
description?: string
|
||||||
attach?: "account_secret" | "account_address" | string;
|
required?: boolean
|
||||||
}
|
}
|
||||||
>;
|
>
|
||||||
|
|
||||||
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
|
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state)
|
||||||
const [templateError, setTemplateError] = useState("");
|
const [templateError, setTemplateError] = useState('')
|
||||||
const getFieldValues = useCallback(() => {
|
const [fields, setFields] = useState<Fields>({})
|
||||||
try {
|
const [iFrameCode, setIframeCode] = useState('')
|
||||||
const parsed = Handlebars.parse(content);
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
const names = parsed.body
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
.filter((i) => i.type === "MustacheStatement")
|
|
||||||
.map((block) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
const type = block.hash?.pairs?.find((i) => i.key == "type");
|
|
||||||
// @ts-expect-error
|
|
||||||
const attach = block.hash?.pairs?.find((i) => i.key == "attach");
|
|
||||||
// @ts-expect-error
|
|
||||||
const label = block.hash?.pairs?.find((i) => i.key == "label");
|
|
||||||
const key =
|
|
||||||
// @ts-expect-error
|
|
||||||
block?.path?.original === "customize_input"
|
|
||||||
? // @ts-expect-error
|
|
||||||
block?.params?.[0].original
|
|
||||||
: // @ts-expect-error
|
|
||||||
block?.path?.original;
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
label: label?.value?.original || key,
|
|
||||||
attach: attach?.value?.original,
|
|
||||||
type: type?.value?.original,
|
|
||||||
value: "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const defaultState: Fields = {};
|
|
||||||
|
|
||||||
if (names) {
|
const getFields = useCallback(() => {
|
||||||
names.forEach((field) => (defaultState[field.key] = field));
|
const inputTags = ['input', 'param', 'arg', 'argument']
|
||||||
|
const tags = getTags(content)
|
||||||
|
.filter(tag => inputTags.includes(tag.tag))
|
||||||
|
.filter(tag => !!tag.name)
|
||||||
|
|
||||||
|
let _fields = tags.map(tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
value: tag.default || '',
|
||||||
|
type: tag.type,
|
||||||
|
description: tag.description,
|
||||||
|
required: !tag.optional
|
||||||
|
}))
|
||||||
|
|
||||||
|
const fields: Fields = _fields.reduce((acc, field) => {
|
||||||
|
acc[field.name] = field
|
||||||
|
return acc
|
||||||
|
}, {} as Fields)
|
||||||
|
|
||||||
|
const error = getErrors(content)
|
||||||
|
if (error) setTemplateError(error.message)
|
||||||
|
else setTemplateError('')
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}, [content])
|
||||||
|
|
||||||
|
const runScript = useCallback(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Show loading toast only after 1 second, otherwise skip it.
|
||||||
|
let loaded = false
|
||||||
|
let toastId: string | undefined;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!loaded) {
|
||||||
|
toastId = toast.loading('Loading packages, may take a few seconds...', {
|
||||||
|
position: 'bottom-center',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
let data: any = {}
|
||||||
|
Object.keys(fields).forEach(key => {
|
||||||
|
data[key] = fields[key].value
|
||||||
|
})
|
||||||
|
const template = await generateHtmlTemplate(content, data)
|
||||||
|
setIframeCode(template)
|
||||||
|
|
||||||
|
loaded = true
|
||||||
|
if (toastId) {
|
||||||
|
toast.dismiss(toastId)
|
||||||
}
|
}
|
||||||
setTemplateError("");
|
state.scriptLogs = [{ type: 'success', message: 'Started running...' }]
|
||||||
return defaultState;
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
setTemplateError("Could not parse template");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}, [content]);
|
|
||||||
|
|
||||||
// const defaultFieldValues = getFieldValues();
|
|
||||||
|
|
||||||
const [fields, setFields] = useState<Fields>({});
|
|
||||||
const [iFrameCode, setIframeCode] = useState("");
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
||||||
const runScript = () => {
|
|
||||||
const fieldsToSend: Record<string, string> = {};
|
|
||||||
Object.entries(fields).map(([key, obj]) => {
|
|
||||||
fieldsToSend[key] = obj.value;
|
|
||||||
});
|
|
||||||
const template = Handlebars.compile(content, { strict: false });
|
|
||||||
try {
|
|
||||||
const code = template(fieldsToSend);
|
|
||||||
setIframeCode(generateHtmlTemplate(code));
|
|
||||||
state.scriptLogs = [
|
|
||||||
...snap.scriptLogs,
|
|
||||||
{ type: "success", message: "Started running..." },
|
|
||||||
];
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.scriptLogs = [
|
state.scriptLogs = [
|
||||||
...snap.scriptLogs,
|
...snap.scriptLogs,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
{ type: "error", message: err?.message || "Could not parse template" },
|
{ type: 'error', message: err?.message || 'Could not parse template' }
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
};
|
setIsLoading(false);
|
||||||
|
}, [content, fields, snap.scriptLogs])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEvent = (e: any) => {
|
const handleEvent = (e: any) => {
|
||||||
if (e.data.type === "log" || e.data.type === "error") {
|
if (e.data.type === 'log' || e.data.type === 'error') {
|
||||||
const data: ILog[] = e.data.args.map((msg: any) => ({
|
const data: ILog[] = e.data.args.map((msg: any) => ({
|
||||||
type: e.data.type,
|
type: e.data.type,
|
||||||
message: typeof msg === "string" ? msg : JSON.stringify(msg, null, 2),
|
message: typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2)
|
||||||
}));
|
}))
|
||||||
state.scriptLogs = [...snap.scriptLogs, ...data];
|
state.scriptLogs = [...snap.scriptLogs, ...data]
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
window.addEventListener("message", handleEvent);
|
window.addEventListener('message', handleEvent)
|
||||||
return () => window.removeEventListener("message", handleEvent);
|
return () => window.removeEventListener('message', handleEvent)
|
||||||
}, [snap.scriptLogs]);
|
}, [snap.scriptLogs])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newDefaultState = getFieldValues();
|
const defaultFields = getFields() || {}
|
||||||
setFields(newDefaultState || {});
|
setFields(defaultFields)
|
||||||
}, [content, setFields, getFieldValues]);
|
}, [content, setFields, getFields])
|
||||||
|
|
||||||
const options = snap.accounts?.map((acc) => ({
|
const accOptions = snap.accounts?.map(acc => ({
|
||||||
|
...acc,
|
||||||
label: acc.name,
|
label: acc.name,
|
||||||
secret: acc.secret,
|
value: acc.address
|
||||||
address: acc.address,
|
}))
|
||||||
value: acc.address,
|
|
||||||
}));
|
const isDisabled = Object.values(fields).some(field => field.required && !field.value)
|
||||||
|
|
||||||
|
const handleRun = useCallback(async () => {
|
||||||
|
if (isDisabled) return toast.error('Please fill in all the required fields.')
|
||||||
|
|
||||||
|
state.scriptLogs = []
|
||||||
|
await runScript();
|
||||||
|
setIsDialogOpen(false)
|
||||||
|
}, [isDisabled, runScript])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -181,8 +210,8 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
saveFile(false);
|
saveFile(false)
|
||||||
setIframeCode("");
|
setIframeCode('')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Play weight="bold" size="16px" /> {name}
|
<Play weight="bold" size="16px" /> {name}
|
||||||
@@ -191,93 +220,87 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Run {name} script</DialogTitle>
|
<DialogTitle>Run {name} script</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
You are about to run scripts provided by the developer of the hook,
|
<Box>
|
||||||
make sure you know what you are doing.
|
You are about to run scripts provided by the developer of the hook, make sure you
|
||||||
<br />
|
trust the author before you continue.
|
||||||
|
</Box>
|
||||||
{templateError && (
|
{templateError && (
|
||||||
<Box
|
<Box
|
||||||
as="span"
|
as="span"
|
||||||
css={{ display: "block", color: "$error", mt: "$3" }}
|
css={{
|
||||||
|
display: 'block',
|
||||||
|
color: '$error',
|
||||||
|
mt: '$3',
|
||||||
|
whiteSpace: 'pre'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Error occured while parsing template, modify script and try
|
{templateError}
|
||||||
again!
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<br />
|
{Object.keys(fields).length > 0 && (
|
||||||
{Object.keys(fields).length > 0
|
<Box css={{ mt: '$4', mb: 0 }}>
|
||||||
? `You also need to fill in following parameters to run the script`
|
Fill in the following parameters to run the script.
|
||||||
: ""}
|
|
||||||
</DialogDescription>
|
|
||||||
<Stack css={{ width: "100%" }}>
|
|
||||||
{Object.keys(fields).map((key) => (
|
|
||||||
<Box key={key} css={{ width: "100%" }}>
|
|
||||||
<label>
|
|
||||||
{fields[key]?.label || key}{" "}
|
|
||||||
{fields[key].attach === "account_secret" &&
|
|
||||||
`(Script uses account secret)`}
|
|
||||||
</label>
|
|
||||||
{fields[key].attach === "account_secret" ||
|
|
||||||
fields[key].attach === "account_address" ? (
|
|
||||||
<Select
|
|
||||||
css={{ mt: "$1" }}
|
|
||||||
options={options}
|
|
||||||
onChange={(val: any) => {
|
|
||||||
setFields({
|
|
||||||
...fields,
|
|
||||||
[key]: {
|
|
||||||
...fields[key],
|
|
||||||
value:
|
|
||||||
fields[key].attach === "account_secret"
|
|
||||||
? val.secret
|
|
||||||
: val.address,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
value={options.find(
|
|
||||||
(opt) =>
|
|
||||||
opt.address === fields[key].value ||
|
|
||||||
opt.secret === fields[key].value
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
type={fields[key].type || "text"}
|
|
||||||
value={
|
|
||||||
typeof fields[key].value !== "string"
|
|
||||||
? // @ts-expect-error
|
|
||||||
fields[key].value.value
|
|
||||||
: fields[key].value
|
|
||||||
}
|
|
||||||
css={{ mt: "$1" }}
|
|
||||||
onChange={(e) => {
|
|
||||||
setFields({
|
|
||||||
...fields,
|
|
||||||
[key]: { ...fields[key], value: e.target.value },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
)}
|
||||||
<Flex
|
</DialogDescription>
|
||||||
css={{ justifyContent: "flex-end", width: "100%", gap: "$3" }}
|
|
||||||
>
|
<Stack css={{ width: '100%' }}>
|
||||||
|
{Object.keys(fields).map(key => {
|
||||||
|
const { name, value, type, description, required } = fields[key]
|
||||||
|
|
||||||
|
const isAccount = type?.startsWith('Account')
|
||||||
|
const isAccountSecret = type === 'Account.secret'
|
||||||
|
|
||||||
|
const accountField = (isAccount && type?.split('.')[1]) || 'address'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={name} css={{ width: '100%' }}>
|
||||||
|
<Label css={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span>
|
||||||
|
{description || name} {required && <Text error>*</Text>}
|
||||||
|
</span>
|
||||||
|
{isAccountSecret && (
|
||||||
|
<Text error small css={{ alignSelf: 'end' }}>
|
||||||
|
can access account secret key
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
{isAccount ? (
|
||||||
|
<Select
|
||||||
|
css={{ mt: '$1' }}
|
||||||
|
options={accOptions}
|
||||||
|
onChange={(val: any) => {
|
||||||
|
setFields({
|
||||||
|
...fields,
|
||||||
|
[key]: {
|
||||||
|
...fields[key],
|
||||||
|
value: val[accountField]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
value={accOptions.find((acc: any) => acc[accountField] === value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
type={type || 'text'}
|
||||||
|
value={value}
|
||||||
|
css={{ mt: '$1' }}
|
||||||
|
onChange={e => {
|
||||||
|
setFields({
|
||||||
|
...fields,
|
||||||
|
[key]: { ...fields[key], value: e.target.value }
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Flex css={{ justifyContent: 'flex-end', width: '100%', gap: '$3' }}>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button outline>Cancel</Button>
|
<Button outline>Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button
|
<Button variant="primary" isDisabled={isDisabled || isLoading} isLoading={isLoading} onClick={handleRun}>
|
||||||
variant="primary"
|
|
||||||
isDisabled={
|
|
||||||
(Object.entries(fields).length > 0 &&
|
|
||||||
Object.entries(fields).some(([key, obj]) => !obj.value)) ||
|
|
||||||
Boolean(templateError)
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
state.scriptLogs = [];
|
|
||||||
runScript();
|
|
||||||
setIsDialogOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Run script
|
Run script
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -285,14 +308,14 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
|
|||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: "$1",
|
top: '$1',
|
||||||
right: "$1",
|
right: '$1',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
background: "$mauve1",
|
background: '$mauve1',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
borderRadius: "$full",
|
borderRadius: '$full',
|
||||||
p: "$1",
|
p: '$1'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X size="20px" />
|
<X size="20px" />
|
||||||
@@ -301,14 +324,10 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
{iFrameCode && (
|
{iFrameCode && (
|
||||||
<iframe
|
<iframe style={{ display: 'none' }} srcDoc={iFrameCode} sandbox="allow-scripts" />
|
||||||
style={{ display: "none" }}
|
|
||||||
srcDoc={iFrameCode}
|
|
||||||
sandbox="allow-scripts"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default RunScript;
|
export default RunScript
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { forwardRef } from "react";
|
import { forwardRef } from 'react'
|
||||||
import { mauve, mauveDark, purple, purpleDark } from "@radix-ui/colors";
|
import { mauve, mauveDark, purple, purpleDark } from '@radix-ui/colors'
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from 'next-themes'
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from 'next/dynamic'
|
||||||
import type { Props } from "react-select";
|
import type { Props } from 'react-select'
|
||||||
const SelectInput = dynamic(() => import("react-select"), { ssr: false });
|
const SelectInput = dynamic(() => import('react-select'), { ssr: false })
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
const Select = forwardRef<any, Props>((props, ref) => {
|
const Select = forwardRef<any, Props>((props, ref) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme()
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === 'dark'
|
||||||
const colors: any = {
|
const colors: any = {
|
||||||
// primary: pink.pink9,
|
// primary: pink.pink9,
|
||||||
active: isDark ? purpleDark.purple9 : purple.purple9,
|
active: isDark ? purpleDark.purple9 : purple.purple9,
|
||||||
@@ -26,102 +26,98 @@ const Select = forwardRef<any, Props>((props, ref) => {
|
|||||||
mauve9: isDark ? mauveDark.mauve9 : mauve.mauve9,
|
mauve9: isDark ? mauveDark.mauve9 : mauve.mauve9,
|
||||||
mauve12: isDark ? mauveDark.mauve12 : mauve.mauve12,
|
mauve12: isDark ? mauveDark.mauve12 : mauve.mauve12,
|
||||||
border: isDark ? mauveDark.mauve10 : mauve.mauve10,
|
border: isDark ? mauveDark.mauve10 : mauve.mauve10,
|
||||||
placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11,
|
placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11
|
||||||
};
|
}
|
||||||
colors.outline = colors.background;
|
colors.outline = colors.background
|
||||||
colors.selected = colors.secondary;
|
colors.selected = colors.secondary
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
ref={ref}
|
ref={ref}
|
||||||
menuPosition={props.menuPosition || "fixed"}
|
menuPosition={props.menuPosition || 'fixed'}
|
||||||
styles={{
|
styles={{
|
||||||
container: (provided) => {
|
container: provided => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
};
|
width: '100%'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
singleValue: (provided) => ({
|
singleValue: provided => ({
|
||||||
...provided,
|
...provided,
|
||||||
color: colors.mauve12,
|
color: colors.mauve12
|
||||||
}),
|
}),
|
||||||
menu: (provided) => ({
|
menu: provided => ({
|
||||||
...provided,
|
...provided,
|
||||||
backgroundColor: colors.dropDownBg,
|
backgroundColor: colors.dropDownBg
|
||||||
}),
|
}),
|
||||||
control: (provided, state) => {
|
control: (provided, state) => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
border: "0px",
|
border: '0px',
|
||||||
backgroundColor: colors.mauve4,
|
backgroundColor: colors.mauve4,
|
||||||
boxShadow: `0 0 0 1px ${
|
boxShadow: `0 0 0 1px ${state.isFocused ? colors.border : colors.secondary}`
|
||||||
state.isFocused ? colors.border : colors.secondary
|
}
|
||||||
}`,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
input: (provided) => {
|
input: provided => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
color: "$text",
|
color: '$text'
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
multiValue: (provided) => {
|
multiValue: provided => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
backgroundColor: colors.mauve8,
|
backgroundColor: colors.mauve8
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
multiValueLabel: (provided) => {
|
multiValueLabel: provided => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
color: colors.mauve12,
|
color: colors.mauve12
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
multiValueRemove: (provided) => {
|
multiValueRemove: provided => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
":hover": {
|
':hover': {
|
||||||
background: colors.mauve9,
|
background: colors.mauve9
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
option: (provided, state) => {
|
option: (provided, state) => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
color: colors.searchText,
|
color: colors.searchText,
|
||||||
backgroundColor:
|
backgroundColor: state.isFocused ? colors.activeLight : colors.dropDownBg,
|
||||||
state.isSelected || state.isFocused
|
':hover': {
|
||||||
? colors.activeLight
|
|
||||||
: colors.dropDownBg,
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: colors.active,
|
backgroundColor: colors.active,
|
||||||
color: "#ffffff",
|
color: '#ffffff'
|
||||||
},
|
},
|
||||||
":selected": {
|
':selected': {
|
||||||
backgroundColor: "red",
|
backgroundColor: 'red'
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
indicatorSeparator: (provided) => {
|
indicatorSeparator: provided => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
backgroundColor: colors.secondary,
|
backgroundColor: colors.secondary
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
dropdownIndicator: (provided, state) => {
|
dropdownIndicator: (provided, state) => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
color: state.isFocused ? colors.border : colors.secondary,
|
color: state.isFocused ? colors.border : colors.secondary,
|
||||||
":hover": {
|
':hover': {
|
||||||
color: colors.border,
|
color: colors.border
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
export default styled(Select, {});
|
export default styled(Select, {})
|
||||||
|
|||||||
71
components/Sequence.tsx
Normal file
71
components/Sequence.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { FC, useCallback, useState } from 'react'
|
||||||
|
import state from '../state'
|
||||||
|
import { Flex, Input, Button } from '.'
|
||||||
|
import fetchAccountInfo from '../utils/accountInfo'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
|
interface AccountSequenceProps {
|
||||||
|
address?: string
|
||||||
|
}
|
||||||
|
const AccountSequence: FC<AccountSequenceProps> = ({ address }) => {
|
||||||
|
const { accounts } = useSnapshot(state)
|
||||||
|
const account = accounts.find(acc => acc.address === address)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const setSequence = useCallback(
|
||||||
|
(sequence: number) => {
|
||||||
|
const acc = state.accounts.find(acc => acc.address == address)
|
||||||
|
if (!acc) return
|
||||||
|
acc.sequence = sequence
|
||||||
|
},
|
||||||
|
[address]
|
||||||
|
)
|
||||||
|
const handleUpdateSequence = useCallback(
|
||||||
|
async (silent?: boolean) => {
|
||||||
|
if (!account) return
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const info = await fetchAccountInfo(account.address, { silent })
|
||||||
|
if (info) {
|
||||||
|
setSequence(info.Sequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
},
|
||||||
|
[account, setSequence]
|
||||||
|
)
|
||||||
|
const disabled = !account
|
||||||
|
return (
|
||||||
|
<Flex row align="center" fluid>
|
||||||
|
<Input
|
||||||
|
placeholder="Account sequence"
|
||||||
|
value={account?.sequence || ""}
|
||||||
|
disabled={!account}
|
||||||
|
type="number"
|
||||||
|
readOnly={true}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="primary"
|
||||||
|
type="button"
|
||||||
|
outline
|
||||||
|
disabled={disabled}
|
||||||
|
isDisabled={disabled}
|
||||||
|
isLoading={isLoading}
|
||||||
|
css={{
|
||||||
|
background: '$backgroundAlt',
|
||||||
|
position: 'absolute',
|
||||||
|
right: '$2',
|
||||||
|
fontSize: '$xs',
|
||||||
|
cursor: 'pointer',
|
||||||
|
alignContent: 'center',
|
||||||
|
display: 'flex'
|
||||||
|
}}
|
||||||
|
onClick={() => handleUpdateSequence()}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountSequence
|
||||||
@@ -1,64 +1,67 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Plus, Trash, X } from "phosphor-react";
|
import { Plus, Trash, X } from 'phosphor-react'
|
||||||
import Button from "./Button";
|
import { Button, Box, Text } from '.'
|
||||||
import Box from "./Box";
|
import { Stack, Flex, Select } from '.'
|
||||||
import { Stack, Flex, Select } from ".";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogTrigger,
|
DialogTrigger
|
||||||
} from "./Dialog";
|
} from './Dialog'
|
||||||
import { Input, Label } from "./Input";
|
import { Input, Label } from './Input'
|
||||||
import {
|
import { Controller, SubmitHandler, useFieldArray, useForm } from 'react-hook-form'
|
||||||
Controller,
|
|
||||||
SubmitHandler,
|
|
||||||
useFieldArray,
|
|
||||||
useForm,
|
|
||||||
} from "react-hook-form";
|
|
||||||
|
|
||||||
import { TTS, tts } from "../utils/hookOnCalculator";
|
import { deployHook } from '../state/actions'
|
||||||
import { deployHook } from "../state/actions";
|
import { useSnapshot } from 'valtio'
|
||||||
import { useSnapshot } from "valtio";
|
import state, { IFile, SelectOption } from '../state'
|
||||||
import state from "../state";
|
import toast from 'react-hot-toast'
|
||||||
import toast from "react-hot-toast";
|
import { prepareDeployHookTx, sha256 } from '../state/actions/deployHook'
|
||||||
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
|
import estimateFee from '../utils/estimateFee'
|
||||||
import estimateFee from "../utils/estimateFee";
|
import { getParameters, getInvokeOptions, transactionOptions, SetHookData } from '../utils/setHook'
|
||||||
|
import { capitalize } from '../utils/helpers'
|
||||||
const transactionOptions = Object.keys(tts).map((key) => ({
|
import AccountSequence from './Sequence'
|
||||||
label: key,
|
|
||||||
value: key as keyof TTS,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export type SetHookData = {
|
|
||||||
Invoke: {
|
|
||||||
value: keyof TTS;
|
|
||||||
label: string;
|
|
||||||
}[];
|
|
||||||
Fee: string;
|
|
||||||
HookNamespace: string;
|
|
||||||
HookParameters: {
|
|
||||||
HookParameter: {
|
|
||||||
HookParameterName: string;
|
|
||||||
HookParameterValue: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
// HookGrants: {
|
|
||||||
// HookGrant: {
|
|
||||||
// Authorize: string;
|
|
||||||
// HookHash: string;
|
|
||||||
// };
|
|
||||||
// }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||||
({ accountAddress }) => {
|
({ accountAddress }) => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state)
|
||||||
const account = snap.accounts.find((acc) => acc.address === accountAddress);
|
|
||||||
|
const [estimateLoading, setEstimateLoading] = useState(false)
|
||||||
|
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false)
|
||||||
|
|
||||||
|
const compiledFiles = snap.files.filter(file => file.compiledContent)
|
||||||
|
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined
|
||||||
|
|
||||||
|
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
|
||||||
|
label: acc.name,
|
||||||
|
value: acc.address
|
||||||
|
}))
|
||||||
|
|
||||||
|
const [selectedAccount, setSelectedAccount] = useState(
|
||||||
|
accountOptions.find(acc => acc.value === accountAddress)
|
||||||
|
)
|
||||||
|
const account = snap.accounts.find(acc => acc.address === selectedAccount?.value)
|
||||||
|
|
||||||
|
const getHookNamespace = useCallback(
|
||||||
|
() =>
|
||||||
|
(activeFile && snap.deployValues[activeFile.name]?.HookNamespace) ||
|
||||||
|
activeFile?.name.split('.')[0] ||
|
||||||
|
'',
|
||||||
|
[activeFile, snap.deployValues]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getDefaultValues = useCallback((): Partial<SetHookData> => {
|
||||||
|
const content = activeFile?.compiledValueSnapshot
|
||||||
|
return (
|
||||||
|
(activeFile && snap.deployValues[activeFile.name]) || {
|
||||||
|
HookNamespace: getHookNamespace(),
|
||||||
|
Invoke: getInvokeOptions(content),
|
||||||
|
HookParameters: getParameters(content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}, [activeFile, getHookNamespace, snap.deployValues])
|
||||||
|
|
||||||
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -66,37 +69,31 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
getValues,
|
getValues,
|
||||||
formState: { errors },
|
reset,
|
||||||
|
formState: { errors }
|
||||||
} = useForm<SetHookData>({
|
} = useForm<SetHookData>({
|
||||||
defaultValues: {
|
defaultValues: getDefaultValues()
|
||||||
HookNamespace:
|
})
|
||||||
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
|
|
||||||
Invoke: transactionOptions.filter((to) => to.label === "ttPAYMENT"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: "HookParameters", // unique name for your Field Array
|
name: 'HookParameters' // unique name for your Field Array
|
||||||
});
|
})
|
||||||
const [formInitialized, setFormInitialized] = useState(false);
|
|
||||||
const [estimateLoading, setEstimateLoading] = useState(false);
|
const watchedFee = watch('Fee')
|
||||||
const watchedFee = watch("Fee");
|
|
||||||
// Update value if activeWat changes
|
// Reset form if activeFile changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(
|
if (!activeFile) return
|
||||||
"HookNamespace",
|
const defaultValues = getDefaultValues()
|
||||||
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
|
|
||||||
);
|
reset(defaultValues)
|
||||||
setFormInitialized(true);
|
}, [activeFile, getDefaultValues, reset])
|
||||||
}, [snap.activeWat, snap.files, setValue]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (watchedFee && (watchedFee.includes('.') || watchedFee.includes(','))) {
|
||||||
watchedFee &&
|
setValue('Fee', watchedFee.replaceAll('.', '').replaceAll(',', ''))
|
||||||
(watchedFee.includes(".") || watchedFee.includes(","))
|
|
||||||
) {
|
|
||||||
setValue("Fee", watchedFee.replaceAll(".", "").replaceAll(",", ""));
|
|
||||||
}
|
}
|
||||||
}, [watchedFee, setValue]);
|
}, [watchedFee, setValue])
|
||||||
// const {
|
// const {
|
||||||
// fields: grantFields,
|
// fields: grantFields,
|
||||||
// append: grantAppend,
|
// append: grantAppend,
|
||||||
@@ -105,67 +102,76 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
// control,
|
// control,
|
||||||
// name: "HookGrants", // unique name for your Field Array
|
// name: "HookGrants", // unique name for your Field Array
|
||||||
// });
|
// });
|
||||||
const [hashedNamespace, setHashedNamespace] = useState("");
|
const [hashedNamespace, setHashedNamespace] = useState('')
|
||||||
const namespace = watch(
|
|
||||||
"HookNamespace",
|
const namespace = watch('HookNamespace', getHookNamespace())
|
||||||
snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
|
|
||||||
);
|
|
||||||
const calculateHashedValue = useCallback(async () => {
|
const calculateHashedValue = useCallback(async () => {
|
||||||
const hashedVal = await sha256(namespace);
|
const hashedVal = await sha256(namespace)
|
||||||
setHashedNamespace(hashedVal.toUpperCase());
|
setHashedNamespace(hashedVal.toUpperCase())
|
||||||
}, [namespace]);
|
}, [namespace])
|
||||||
useEffect(() => {
|
|
||||||
calculateHashedValue();
|
|
||||||
}, [namespace, calculateHashedValue]);
|
|
||||||
|
|
||||||
// Calcucate initial fee estimate when modal opens
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formInitialized && account) {
|
calculateHashedValue()
|
||||||
(async () => {
|
}, [namespace, calculateHashedValue])
|
||||||
const formValues = getValues();
|
|
||||||
const tx = await prepareDeployHookTx(account, formValues);
|
const calculateFee = useCallback(async () => {
|
||||||
if (!tx) {
|
if (!account) return
|
||||||
return;
|
|
||||||
}
|
const formValues = getValues()
|
||||||
const res = await estimateFee(tx, account);
|
const tx = await prepareDeployHookTx(account, formValues)
|
||||||
if (res && res.base_fee) {
|
if (!tx) {
|
||||||
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
|
return
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
const res = await estimateFee(tx, account)
|
||||||
}, [formInitialized]);
|
if (res && res.base_fee) {
|
||||||
|
setValue('Fee', Math.round(Number(res.base_fee || '')).toString())
|
||||||
|
}
|
||||||
|
}, [account, getValues, setValue])
|
||||||
|
|
||||||
if (!account) {
|
const tooLargeFile = () => {
|
||||||
return null;
|
return Boolean(
|
||||||
|
activeFile?.compiledContent?.byteLength && activeFile?.compiledContent?.byteLength >= 64000
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<SetHookData> = async (data) => {
|
const onSubmit: SubmitHandler<SetHookData> = async data => {
|
||||||
const currAccount = state.accounts.find(
|
const currAccount = state.accounts.find(acc => acc.address === account?.address)
|
||||||
(acc) => acc.address === account.address
|
if (!account) return
|
||||||
);
|
if (currAccount) currAccount.isLoading = true
|
||||||
if (currAccount) currAccount.isLoading = true;
|
|
||||||
const res = await deployHook(account, data);
|
|
||||||
if (currAccount) currAccount.isLoading = false;
|
|
||||||
|
|
||||||
if (res && res.engine_result === "tesSUCCESS") {
|
data.HookParameters.forEach(param => {
|
||||||
toast.success("Transaction succeeded!");
|
delete param.$metaData
|
||||||
return setIsSetHookDialogOpen(false);
|
return param
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await deployHook(account, data)
|
||||||
|
if (currAccount) currAccount.isLoading = false
|
||||||
|
|
||||||
|
if (res && res.engine_result === 'tesSUCCESS') {
|
||||||
|
toast.success('Transaction succeeded!')
|
||||||
|
return setIsSetHookDialogOpen(false)
|
||||||
}
|
}
|
||||||
toast.error(`Transaction failed! (${res?.engine_result_message})`);
|
toast.error(`Transaction failed! (${res?.engine_result_message})`)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const onOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
setIsSetHookDialogOpen(open)
|
||||||
|
|
||||||
|
if (open) calculateFee()
|
||||||
|
},
|
||||||
|
[calculateFee]
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
|
<Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
ghost
|
ghost
|
||||||
size="xs"
|
size="xs"
|
||||||
uppercase
|
uppercase
|
||||||
variant={"secondary"}
|
variant={'secondary'}
|
||||||
disabled={
|
disabled={!account || account.isLoading || !activeFile || tooLargeFile()}
|
||||||
account.isLoading ||
|
|
||||||
!snap.files.filter((file) => file.compiledWatContent).length
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Set Hook
|
Set Hook
|
||||||
</Button>
|
</Button>
|
||||||
@@ -174,15 +180,26 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<DialogTitle>Deploy configuration</DialogTitle>
|
<DialogTitle>Deploy configuration</DialogTitle>
|
||||||
<DialogDescription as="div">
|
<DialogDescription as="div">
|
||||||
<Stack css={{ width: "100%", flex: 1 }}>
|
<Stack css={{ width: '100%', flex: 1 }}>
|
||||||
<Box css={{ width: "100%" }}>
|
<Box css={{ width: '100%' }}>
|
||||||
|
<Label>Account</Label>
|
||||||
|
<Select
|
||||||
|
instanceId="deploy-account"
|
||||||
|
placeholder="Select account"
|
||||||
|
options={accountOptions}
|
||||||
|
value={selectedAccount}
|
||||||
|
onChange={(acc: any) => setSelectedAccount(acc)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box css={{ width: '100%', position: 'relative' }}>
|
||||||
|
<Label>Sequence</Label>
|
||||||
|
<AccountSequence address={selectedAccount?.value} />
|
||||||
|
</Box>
|
||||||
|
<Box css={{ width: '100%' }}>
|
||||||
<Label>Invoke on transactions</Label>
|
<Label>Invoke on transactions</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="Invoke"
|
name="Invoke"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={transactionOptions.filter(
|
|
||||||
(to) => to.label === "ttPAYMENT"
|
|
||||||
)}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
@@ -194,49 +211,51 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box css={{ width: "100%" }}>
|
<Box css={{ width: '100%' }}>
|
||||||
<Label>Hook Namespace Seed</Label>
|
<Label>Hook Namespace Seed</Label>
|
||||||
<Input
|
<Input {...register('HookNamespace', { required: true })} autoComplete={'off'} />
|
||||||
{...register("HookNamespace", { required: true })}
|
{errors.HookNamespace?.type === 'required' && (
|
||||||
autoComplete={"off"}
|
<Box css={{ display: 'inline', color: '$red11' }}>Namespace is required</Box>
|
||||||
defaultValue={
|
|
||||||
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{errors.HookNamespace?.type === "required" && (
|
|
||||||
<Box css={{ display: "inline", color: "$red11" }}>
|
|
||||||
Namespace is required
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
<Box css={{ mt: "$3" }}>
|
<Box css={{ mt: '$3' }}>
|
||||||
<Label>Hook Namespace (sha256)</Label>
|
<Label>Hook Namespace (sha256)</Label>
|
||||||
<Input readOnly value={hashedNamespace} />
|
<Input readOnly value={hashedNamespace} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box css={{ width: "100%" }}>
|
<Box css={{ width: '100%' }}>
|
||||||
<Label style={{ marginBottom: "10px", display: "block" }}>
|
<Label style={{ marginBottom: '10px', display: 'block' }}>Hook parameters</Label>
|
||||||
Hook parameters
|
|
||||||
</Label>
|
|
||||||
<Stack>
|
<Stack>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<Stack key={field.id}>
|
<Stack key={field.id}>
|
||||||
<Input
|
<Flex column>
|
||||||
// important to include key with field's id
|
<Flex row>
|
||||||
placeholder="Parameter name"
|
<Input
|
||||||
{...register(
|
// important to include key with field's id
|
||||||
`HookParameters.${index}.HookParameter.HookParameterName`
|
placeholder="Parameter name"
|
||||||
)}
|
readOnly={field.$metaData?.required}
|
||||||
/>
|
{...register(
|
||||||
<Input
|
`HookParameters.${index}.HookParameter.HookParameterName`
|
||||||
placeholder="Value (hex-quoted)"
|
)}
|
||||||
{...register(
|
/>
|
||||||
`HookParameters.${index}.HookParameter.HookParameterValue`
|
<Input
|
||||||
)}
|
css={{ mx: '$2' }}
|
||||||
/>
|
placeholder="Value (hex-quoted)"
|
||||||
<Button onClick={() => remove(index)} variant="destroy">
|
{...register(
|
||||||
<Trash weight="regular" size="16px" />
|
`HookParameters.${index}.HookParameter.HookParameterValue`,
|
||||||
</Button>
|
{ required: field.$metaData?.required }
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => remove(index)} variant="destroy">
|
||||||
|
<Trash weight="regular" size="16px" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
{errors.HookParameters?.[index]?.HookParameter?.HookParameterValue
|
||||||
|
?.type === 'required' && <Text error>This field is required</Text>}
|
||||||
|
<Label css={{ fontSize: '$sm', mt: '$1' }}>
|
||||||
|
{capitalize(field.$metaData?.description)}
|
||||||
|
</Label>
|
||||||
|
</Flex>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
@@ -246,9 +265,9 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
append({
|
append({
|
||||||
HookParameter: {
|
HookParameter: {
|
||||||
HookParameterName: "",
|
HookParameterName: '',
|
||||||
HookParameterValue: "",
|
HookParameterValue: ''
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -257,30 +276,30 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
<Box css={{ width: "100%", position: "relative" }}>
|
<Box css={{ width: '100%', position: 'relative' }}>
|
||||||
<Label>Fee</Label>
|
<Label>Fee</Label>
|
||||||
<Box css={{ display: "flex", alignItems: "center" }}>
|
<Box css={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
{...register("Fee", { required: true })}
|
{...register('Fee', { required: true })}
|
||||||
autoComplete={"off"}
|
autoComplete={'off'}
|
||||||
onKeyPress={(e) => {
|
onKeyPress={e => {
|
||||||
if (e.key === "." || e.key === ",") {
|
if (e.key === '.' || e.key === ',') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
step="1"
|
step="1"
|
||||||
defaultValue={10000}
|
defaultValue={10000}
|
||||||
css={{
|
css={{
|
||||||
"-moz-appearance": "textfield",
|
'-moz-appearance': 'textfield',
|
||||||
"&::-webkit-outer-spin-button": {
|
'&::-webkit-outer-spin-button': {
|
||||||
"-webkit-appearance": "none",
|
'-webkit-appearance': 'none',
|
||||||
margin: 0,
|
margin: 0
|
||||||
},
|
|
||||||
"&::-webkit-inner-spin-button ": {
|
|
||||||
"-webkit-appearance": "none",
|
|
||||||
margin: 0,
|
|
||||||
},
|
},
|
||||||
|
'&::-webkit-inner-spin-button ': {
|
||||||
|
'-webkit-appearance': 'none',
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -289,46 +308,37 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
outline
|
outline
|
||||||
isLoading={estimateLoading}
|
isLoading={estimateLoading}
|
||||||
css={{
|
css={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
right: "$2",
|
right: '$2',
|
||||||
fontSize: "$xs",
|
fontSize: '$xs',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
alignContent: "center",
|
alignContent: 'center',
|
||||||
display: "flex",
|
display: 'flex'
|
||||||
}}
|
}}
|
||||||
onClick={async (e) => {
|
onClick={async e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setEstimateLoading(true);
|
if (!account) return
|
||||||
const formValues = getValues();
|
setEstimateLoading(true)
|
||||||
|
const formValues = getValues()
|
||||||
try {
|
try {
|
||||||
const tx = await prepareDeployHookTx(
|
const tx = await prepareDeployHookTx(account, formValues)
|
||||||
account,
|
|
||||||
formValues
|
|
||||||
);
|
|
||||||
if (tx) {
|
if (tx) {
|
||||||
const res = await estimateFee(tx, account);
|
const res = await estimateFee(tx, account)
|
||||||
|
|
||||||
if (res && res.base_fee) {
|
if (res && res.base_fee) {
|
||||||
setValue(
|
setValue('Fee', Math.round(Number(res.base_fee || '')).toString())
|
||||||
"Fee",
|
|
||||||
Math.round(
|
|
||||||
Number(res.base_fee || "")
|
|
||||||
).toString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
|
|
||||||
setEstimateLoading(false);
|
setEstimateLoading(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Suggest
|
Suggest
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{errors.Fee?.type === "required" && (
|
{errors.Fee?.type === 'required' && (
|
||||||
<Box css={{ display: "inline", color: "$red11" }}>
|
<Box css={{ display: 'inline', color: '$red11' }}>Fee is required</Box>
|
||||||
Fee is required
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{/* <Box css={{ width: "100%" }}>
|
{/* <Box css={{ width: "100%" }}>
|
||||||
@@ -385,35 +395,31 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
marginTop: 25,
|
marginTop: 25,
|
||||||
justifyContent: "flex-end",
|
justifyContent: 'flex-end',
|
||||||
gap: "$3",
|
gap: '$3'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button outline>Cancel</Button>
|
<Button outline>Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
{/* <DialogClose asChild> */}
|
{/* <DialogClose asChild> */}
|
||||||
<Button
|
<Button variant="primary" type="submit" isLoading={account?.isLoading}>
|
||||||
variant="primary"
|
|
||||||
type="submit"
|
|
||||||
isLoading={account.isLoading}
|
|
||||||
>
|
|
||||||
Set Hook
|
Set Hook
|
||||||
</Button>
|
</Button>
|
||||||
{/* </DialogClose> */}
|
{/* </DialogClose> */}
|
||||||
</Flex>
|
</Flex>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
|
||||||
<X size="20px" />
|
<X size="20px" />
|
||||||
</Box>
|
</Box>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
SetHookDialog.displayName = "SetHookDialog";
|
SetHookDialog.displayName = 'SetHookDialog'
|
||||||
|
|
||||||
export default SetHookDialog;
|
export default SetHookDialog
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Spinner as SpinnerIcon } from "phosphor-react";
|
import { Spinner as SpinnerIcon } from 'phosphor-react'
|
||||||
import { styled, keyframes } from "../stitches.config";
|
import { styled, keyframes } from '../stitches.config'
|
||||||
|
|
||||||
const rotate = keyframes({
|
const rotate = keyframes({
|
||||||
"0%": { transform: "rotate(0deg)" },
|
'0%': { transform: 'rotate(0deg)' },
|
||||||
"100%": { transform: "rotate(-360deg)" },
|
'100%': { transform: 'rotate(-360deg)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const Spinner = styled(SpinnerIcon, {
|
const Spinner = styled(SpinnerIcon, {
|
||||||
animation: `${rotate} 150ms linear infinite`,
|
animation: `${rotate} 150ms linear infinite`,
|
||||||
fontSize: "16px",
|
fontSize: '16px'
|
||||||
});
|
})
|
||||||
|
|
||||||
export default Spinner;
|
export default Spinner
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import Box from "./Box";
|
import Box from './Box'
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
const StackComponent = styled(Box, {
|
const StackComponent = styled(Box, {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexWrap: "wrap",
|
flexWrap: 'wrap',
|
||||||
flexDirection: "row",
|
flexDirection: 'row',
|
||||||
gap: "$4",
|
gap: '$4'
|
||||||
});
|
})
|
||||||
|
|
||||||
export default StackComponent;
|
export default StackComponent
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
import * as SwitchPrimitive from '@radix-ui/react-switch'
|
||||||
|
|
||||||
const StyledSwitch = styled(SwitchPrimitive.Root, {
|
const StyledSwitch = styled(SwitchPrimitive.Root, {
|
||||||
all: "unset",
|
all: 'unset',
|
||||||
width: 42,
|
width: 42,
|
||||||
height: 25,
|
height: 25,
|
||||||
backgroundColor: "$mauve9",
|
backgroundColor: '$mauve9',
|
||||||
borderRadius: "9999px",
|
borderRadius: '9999px',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
boxShadow: `0 2px 10px $colors$mauve2`,
|
boxShadow: `0 2px 10px $colors$mauve2`,
|
||||||
WebkitTapHighlightColor: "rgba(0, 0, 0, 0)",
|
WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)',
|
||||||
"&:focus": { boxShadow: `0 0 0 2px $colors$mauveA2` },
|
'&:focus': { boxShadow: `0 0 0 2px $colors$mauveA2` },
|
||||||
'&[data-state="checked"]': { backgroundColor: "$green11" },
|
'&[data-state="checked"]': { backgroundColor: '$green11' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledThumb = styled(SwitchPrimitive.Thumb, {
|
const StyledThumb = styled(SwitchPrimitive.Thumb, {
|
||||||
display: "block",
|
display: 'block',
|
||||||
width: 21,
|
width: 21,
|
||||||
height: 21,
|
height: 21,
|
||||||
backgroundColor: "white",
|
backgroundColor: 'white',
|
||||||
borderRadius: "9999px",
|
borderRadius: '9999px',
|
||||||
boxShadow: `0 2px 2px $colors$mauveA6`,
|
boxShadow: `0 2px 2px $colors$mauveA6`,
|
||||||
transition: "transform 100ms",
|
transition: 'transform 100ms',
|
||||||
transform: "translateX(2px)",
|
transform: 'translateX(2px)',
|
||||||
willChange: "transform",
|
willChange: 'transform',
|
||||||
'&[data-state="checked"]': { transform: "translateX(19px)" },
|
'&[data-state="checked"]': { transform: 'translateX(19px)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
// Exports
|
// Exports
|
||||||
export const Switch = StyledSwitch;
|
export const Switch = StyledSwitch
|
||||||
export const SwitchThumb = StyledThumb;
|
export const SwitchThumb = StyledThumb
|
||||||
|
|||||||
@@ -1,51 +1,58 @@
|
|||||||
import React, {
|
import React, { useEffect, useState, Fragment, isValidElement, useCallback } from 'react'
|
||||||
useEffect,
|
import type { ReactNode, ReactElement } from 'react'
|
||||||
useState,
|
import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from '.'
|
||||||
Fragment,
|
|
||||||
isValidElement,
|
|
||||||
useCallback,
|
|
||||||
} from "react";
|
|
||||||
import type { ReactNode, ReactElement } from "react";
|
|
||||||
import { Box, Button, Flex, Input, Label, Stack, Text } from ".";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogClose,
|
DialogClose
|
||||||
} from "./Dialog";
|
} from './Dialog'
|
||||||
import { Plus, X } from "phosphor-react";
|
import { Plus, X } from 'phosphor-react'
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
import { capitalize, getFileExtention } from '../utils/helpers'
|
||||||
|
import ContextMenu, { ContentMenuOption } from './ContextMenu'
|
||||||
|
|
||||||
const ErrorText = styled(Text, {
|
const ErrorText = styled(Text, {
|
||||||
color: "$error",
|
color: '$error',
|
||||||
mt: "$1",
|
mt: '$1',
|
||||||
display: "block",
|
display: 'block'
|
||||||
});
|
})
|
||||||
|
|
||||||
|
type Nullable<T> = T | null | undefined | false
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
header?: string;
|
header: string
|
||||||
children: ReactNode;
|
children?: ReactNode
|
||||||
|
renameDisabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO customise messages shown
|
// TODO customize messages shown
|
||||||
interface Props {
|
interface Props {
|
||||||
activeIndex?: number;
|
label?: string
|
||||||
activeHeader?: string;
|
activeIndex?: number
|
||||||
headless?: boolean;
|
activeHeader?: string
|
||||||
children: ReactElement<TabProps>[];
|
headless?: boolean
|
||||||
keepAllAlive?: boolean;
|
children: ReactElement<TabProps>[]
|
||||||
defaultExtension?: string;
|
keepAllAlive?: boolean
|
||||||
forceDefaultExtension?: boolean;
|
defaultExtension?: string
|
||||||
onCreateNewTab?: (name: string) => any;
|
extensionRequired?: boolean
|
||||||
onCloseTab?: (index: number, header?: string) => any;
|
allowedExtensions?: string[]
|
||||||
onChangeActive?: (index: number, header?: string) => any;
|
headerExtraValidation?: {
|
||||||
|
regex: string | RegExp
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
onCreateNewTab?: (name: string) => any
|
||||||
|
onRenameTab?: (index: number, nwName: string, oldName?: string) => any
|
||||||
|
onCloseTab?: (index: number, header?: string) => any
|
||||||
|
onChangeActive?: (index: number, header?: string) => any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tab = (props: TabProps) => null;
|
export const Tab = (props: TabProps) => null
|
||||||
|
|
||||||
export const Tabs = ({
|
export const Tabs = ({
|
||||||
|
label = 'Tab',
|
||||||
children,
|
children,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
activeHeader,
|
activeHeader,
|
||||||
@@ -54,182 +61,229 @@ export const Tabs = ({
|
|||||||
onCreateNewTab,
|
onCreateNewTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onChangeActive,
|
onChangeActive,
|
||||||
defaultExtension = "",
|
onRenameTab,
|
||||||
forceDefaultExtension,
|
headerExtraValidation,
|
||||||
|
extensionRequired,
|
||||||
|
defaultExtension = '',
|
||||||
|
allowedExtensions
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [active, setActive] = useState(activeIndex || 0);
|
const [active, setActive] = useState(activeIndex || 0)
|
||||||
const tabs: TabProps[] = children.map(elem => elem.props);
|
const tabs: TabProps[] = children.map(elem => elem.props)
|
||||||
|
|
||||||
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
|
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false)
|
||||||
const [tabname, setTabname] = useState("");
|
const [renamingTab, setRenamingTab] = useState<number | null>(null)
|
||||||
const [newtabError, setNewtabError] = useState<string | null>(null);
|
const [tabname, setTabname] = useState('')
|
||||||
|
const [tabnameError, setTabnameError] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeIndex) setActive(activeIndex);
|
if (activeIndex) setActive(activeIndex)
|
||||||
}, [activeIndex]);
|
}, [activeIndex])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeHeader) {
|
if (activeHeader) {
|
||||||
const idx = tabs.findIndex(tab => tab.header === activeHeader);
|
const idx = tabs.findIndex(tab => tab.header === activeHeader)
|
||||||
if (idx !== -1) setActive(idx);
|
if (idx !== -1) setActive(idx)
|
||||||
else setActive(0);
|
else setActive(0)
|
||||||
}
|
}
|
||||||
}, [activeHeader, tabs]);
|
}, [activeHeader, tabs])
|
||||||
|
|
||||||
// when filename changes, reset error
|
// when filename changes, reset error
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNewtabError(null);
|
setTabnameError(null)
|
||||||
}, [tabname, setNewtabError]);
|
}, [tabname, setTabnameError])
|
||||||
|
|
||||||
const validateTabname = useCallback(
|
const validateTabname = useCallback(
|
||||||
(tabname: string): { error: string | null } => {
|
(tabname: string): { error?: string; result?: string } => {
|
||||||
if (tabs.find(tab => tab.header === tabname)) {
|
if (!tabname) {
|
||||||
return { error: "Name already exists." };
|
return { error: `Please enter ${label.toLocaleLowerCase()} name.` }
|
||||||
}
|
}
|
||||||
return { error: null };
|
let ext = getFileExtention(tabname)
|
||||||
|
|
||||||
|
if (!ext && defaultExtension) {
|
||||||
|
ext = defaultExtension
|
||||||
|
tabname = `${tabname}.${defaultExtension}`
|
||||||
|
}
|
||||||
|
if (tabs.find(tab => tab.header === tabname)) {
|
||||||
|
return { error: `${capitalize(label)} name already exists.` }
|
||||||
|
}
|
||||||
|
if (extensionRequired && !ext) {
|
||||||
|
return { error: 'File extension is required!' }
|
||||||
|
}
|
||||||
|
if (allowedExtensions && ext && !allowedExtensions.includes(ext)) {
|
||||||
|
return { error: 'This file extension is not allowed!' }
|
||||||
|
}
|
||||||
|
if (headerExtraValidation && !tabname.match(headerExtraValidation.regex)) {
|
||||||
|
return { error: headerExtraValidation.error }
|
||||||
|
}
|
||||||
|
return { result: tabname }
|
||||||
},
|
},
|
||||||
[tabs]
|
[allowedExtensions, defaultExtension, extensionRequired, headerExtraValidation, label, tabs]
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleActiveChange = useCallback(
|
const handleActiveChange = useCallback(
|
||||||
(idx: number, header?: string) => {
|
(idx: number, header?: string) => {
|
||||||
setActive(idx);
|
setActive(idx)
|
||||||
onChangeActive?.(idx, header);
|
onChangeActive?.(idx, header)
|
||||||
},
|
},
|
||||||
[onChangeActive]
|
[onChangeActive]
|
||||||
);
|
)
|
||||||
|
|
||||||
|
const handleRenameTab = useCallback(() => {
|
||||||
|
if (renamingTab === null) return
|
||||||
|
|
||||||
|
const res = validateTabname(tabname)
|
||||||
|
if (res.error) {
|
||||||
|
setTabnameError(`Error: ${res.error}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result: nwName = tabname } = res
|
||||||
|
|
||||||
|
setRenamingTab(null)
|
||||||
|
setTabname('')
|
||||||
|
|
||||||
|
const oldName = tabs[renamingTab]?.header
|
||||||
|
onRenameTab?.(renamingTab, nwName, oldName)
|
||||||
|
|
||||||
|
handleActiveChange(renamingTab, nwName)
|
||||||
|
}, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname])
|
||||||
|
|
||||||
const handleCreateTab = useCallback(() => {
|
const handleCreateTab = useCallback(() => {
|
||||||
// add default extension in case omitted
|
const res = validateTabname(tabname)
|
||||||
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
|
if (res.error) {
|
||||||
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
|
setTabnameError(`Error: ${res.error}`)
|
||||||
_tabname = _tabname + defaultExtension;
|
return
|
||||||
}
|
}
|
||||||
|
const { result: _tabname = tabname } = res
|
||||||
|
|
||||||
const chk = validateTabname(_tabname);
|
setIsNewtabDialogOpen(false)
|
||||||
if (chk.error) {
|
setTabname('')
|
||||||
setNewtabError(`Error: ${chk.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsNewtabDialogOpen(false);
|
onCreateNewTab?.(_tabname)
|
||||||
setTabname("");
|
|
||||||
|
|
||||||
onCreateNewTab?.(_tabname);
|
handleActiveChange(tabs.length, _tabname)
|
||||||
|
}, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length])
|
||||||
// switch to new tab?
|
|
||||||
handleActiveChange(tabs.length, _tabname);
|
|
||||||
}, [
|
|
||||||
tabname,
|
|
||||||
defaultExtension,
|
|
||||||
forceDefaultExtension,
|
|
||||||
validateTabname,
|
|
||||||
onCreateNewTab,
|
|
||||||
handleActiveChange,
|
|
||||||
tabs.length,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleCloseTab = useCallback(
|
const handleCloseTab = useCallback(
|
||||||
(idx: number) => {
|
(idx: number) => {
|
||||||
if (idx <= active && active !== 0) {
|
onCloseTab?.(idx, tabs[idx].header)
|
||||||
setActive(active - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
onCloseTab?.(idx, tabs[idx].header);
|
if (idx <= active && active !== 0) {
|
||||||
|
const nwActive = active - 1
|
||||||
|
handleActiveChange(nwActive, tabs[nwActive].header)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[active, onCloseTab, tabs]
|
[active, handleActiveChange, onCloseTab, tabs]
|
||||||
);
|
)
|
||||||
|
|
||||||
|
const closeOption = (idx: number): Nullable<ContentMenuOption> =>
|
||||||
|
onCloseTab && {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Close',
|
||||||
|
key: 'close',
|
||||||
|
onSelect: () => handleCloseTab(idx)
|
||||||
|
}
|
||||||
|
const renameOption = (idx: number, tab: TabProps): Nullable<ContentMenuOption> => {
|
||||||
|
return (
|
||||||
|
onRenameTab &&
|
||||||
|
!tab.renameDisabled && {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Rename',
|
||||||
|
key: 'rename',
|
||||||
|
onSelect: () => setRenamingTab(idx)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!headless && (
|
{!headless && (
|
||||||
<Stack
|
<Stack
|
||||||
css={{
|
css={{
|
||||||
gap: "$3",
|
gap: '$3',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexWrap: "nowrap",
|
flexWrap: 'nowrap',
|
||||||
marginBottom: "$2",
|
marginBottom: '$2',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
overflow: "auto",
|
overflow: 'auto'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, idx) => (
|
{tabs.map((tab, idx) => (
|
||||||
<Button
|
<ContextMenu
|
||||||
key={tab.header}
|
key={tab.header}
|
||||||
role="tab"
|
options={
|
||||||
tabIndex={idx}
|
[closeOption(idx), renameOption(idx, tab)].filter(Boolean) as ContentMenuOption[]
|
||||||
onClick={() => handleActiveChange(idx, tab.header)}
|
}
|
||||||
onKeyPress={() => handleActiveChange(idx, tab.header)}
|
|
||||||
outline={active !== idx}
|
|
||||||
size="sm"
|
|
||||||
css={{
|
|
||||||
"&:hover": {
|
|
||||||
span: {
|
|
||||||
visibility: "visible",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{tab.header || idx}
|
<Button
|
||||||
{onCloseTab && (
|
role="tab"
|
||||||
<Box
|
tabIndex={idx}
|
||||||
as="span"
|
onClick={() => handleActiveChange(idx, tab.header)}
|
||||||
css={{
|
onKeyPress={() => handleActiveChange(idx, tab.header)}
|
||||||
display: "flex",
|
outline={active !== idx}
|
||||||
p: "2px",
|
size="sm"
|
||||||
borderRadius: "$full",
|
css={{
|
||||||
mr: "-4px",
|
'&:hover': {
|
||||||
"&:hover": {
|
span: {
|
||||||
// boxSizing: "0px 0px 1px",
|
visibility: 'visible'
|
||||||
backgroundColor: "$mauve2",
|
}
|
||||||
color: "$mauve12",
|
}
|
||||||
},
|
}}
|
||||||
}}
|
>
|
||||||
onClick={(ev: React.MouseEvent<HTMLElement>) => {
|
{tab.header || idx}
|
||||||
ev.stopPropagation();
|
{onCloseTab && (
|
||||||
handleCloseTab(idx);
|
<Box
|
||||||
}}
|
as="span"
|
||||||
>
|
css={{
|
||||||
<X size="9px" weight="bold" />
|
display: 'flex',
|
||||||
</Box>
|
p: '2px',
|
||||||
)}
|
borderRadius: '$full',
|
||||||
</Button>
|
mr: '-4px',
|
||||||
|
'&:hover': {
|
||||||
|
// boxSizing: "0px 0px 1px",
|
||||||
|
backgroundColor: '$mauve2',
|
||||||
|
color: '$mauve12'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={(ev: React.MouseEvent<HTMLElement>) => {
|
||||||
|
ev.stopPropagation()
|
||||||
|
handleCloseTab(idx)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size="9px" weight="bold" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</ContextMenu>
|
||||||
))}
|
))}
|
||||||
{onCreateNewTab && (
|
{onCreateNewTab && (
|
||||||
<Dialog
|
<Dialog open={isNewtabDialogOpen} onOpenChange={setIsNewtabDialogOpen}>
|
||||||
open={isNewtabDialogOpen}
|
|
||||||
onOpenChange={setIsNewtabDialogOpen}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button ghost size="sm" css={{ alignItems: 'center', px: '$2', mr: '$3' }}>
|
||||||
ghost
|
<Plus size="16px" /> {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
|
||||||
size="sm"
|
|
||||||
css={{ alignItems: "center", px: "$2", mr: "$3" }}
|
|
||||||
>
|
|
||||||
<Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Create new tab</DialogTitle>
|
<DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Label>Tabname</Label>
|
<Label>{label} name</Label>
|
||||||
<Input
|
<Input
|
||||||
value={tabname}
|
value={tabname}
|
||||||
onChange={e => setTabname(e.target.value)}
|
onChange={e => setTabname(e.target.value)}
|
||||||
onKeyPress={e => {
|
onKeyPress={e => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === 'Enter') {
|
||||||
handleCreateTab();
|
handleCreateTab()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ErrorText>{newtabError}</ErrorText>
|
<ErrorText>{tabnameError}</ErrorText>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
marginTop: 25,
|
marginTop: 25,
|
||||||
justifyContent: "flex-end",
|
justifyContent: 'flex-end',
|
||||||
gap: "$3",
|
gap: '$3'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
@@ -240,7 +294,49 @@ export const Tabs = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
|
||||||
|
<X size="20px" />
|
||||||
|
</Box>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
{onRenameTab && (
|
||||||
|
<Dialog open={renamingTab !== null} onOpenChange={() => setRenamingTab(null)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>
|
||||||
|
Rename <Pre>{tabs[renamingTab || 0]?.header}</Pre>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Label>Enter new name</Label>
|
||||||
|
<Input
|
||||||
|
value={tabname}
|
||||||
|
onChange={e => setTabname(e.target.value)}
|
||||||
|
onKeyPress={e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleRenameTab()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ErrorText>{tabnameError}</ErrorText>
|
||||||
|
</DialogDescription>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
css={{
|
||||||
|
marginTop: 25,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: '$3'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button outline>Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button variant="primary" onClick={handleRenameTab}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Box css={{ position: 'absolute', top: '$3', right: '$3' }}>
|
||||||
<X size="20px" />
|
<X size="20px" />
|
||||||
</Box>
|
</Box>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
@@ -249,29 +345,30 @@ export const Tabs = ({
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{keepAllAlive ? (
|
{keepAllAlive
|
||||||
tabs.map((tab, idx) => {
|
? tabs.map((tab, idx) => {
|
||||||
// TODO Maybe rule out fragments as children
|
// TODO Maybe rule out fragments as children
|
||||||
if (!isValidElement(tab.children)) {
|
if (!isValidElement(tab.children)) {
|
||||||
if (active !== idx) return null;
|
if (active !== idx) return null
|
||||||
return tab.children;
|
return tab.children
|
||||||
}
|
}
|
||||||
let key = tab.children.key || tab.header || idx;
|
let key = tab.children.key || tab.header || idx
|
||||||
let { children } = tab;
|
let { children } = tab
|
||||||
let { style, ...props } = children.props;
|
let { style, ...props } = children.props
|
||||||
return (
|
return (
|
||||||
<children.type
|
<children.type
|
||||||
key={key}
|
key={key}
|
||||||
{...props}
|
{...props}
|
||||||
style={{ ...style, display: active !== idx ? "none" : undefined }}
|
style={{
|
||||||
/>
|
...style,
|
||||||
);
|
display: active !== idx ? 'none' : undefined
|
||||||
})
|
}}
|
||||||
) : (
|
/>
|
||||||
<Fragment key={tabs[active].header || active}>
|
)
|
||||||
{tabs[active].children}
|
})
|
||||||
</Fragment>
|
: tabs[active] && (
|
||||||
)}
|
<Fragment key={tabs[active].header || active}>{tabs[active].children}</Fragment>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
const Text = styled("span", {
|
const Text = styled('span', {
|
||||||
fontFamily: "$body",
|
fontFamily: '$body',
|
||||||
lineHeight: "$body",
|
lineHeight: '$body',
|
||||||
color: "$text",
|
color: '$text',
|
||||||
variants: {
|
variants: {
|
||||||
small: {
|
small: {
|
||||||
true: {
|
true: {
|
||||||
@@ -15,12 +15,27 @@ const Text = styled("span", {
|
|||||||
color: '$mauve9'
|
color: '$mauve9'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
true: {
|
||||||
|
color: '$error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
true: {
|
||||||
|
color: '$warning'
|
||||||
|
}
|
||||||
|
},
|
||||||
monospace: {
|
monospace: {
|
||||||
true: {
|
true: {
|
||||||
fontFamily: '$monospace'
|
fontFamily: '$monospace'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
true: {
|
||||||
|
display: 'block'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default Text;
|
export default Text
|
||||||
|
|||||||
@@ -1,115 +1,113 @@
|
|||||||
import { styled } from "../stitches.config";
|
import { styled } from '../stitches.config'
|
||||||
|
|
||||||
export const Textarea = styled("textarea", {
|
export const Textarea = styled('textarea', {
|
||||||
// Reset
|
// Reset
|
||||||
appearance: "none",
|
appearance: 'none',
|
||||||
borderWidth: "0",
|
borderWidth: '0',
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box',
|
||||||
fontFamily: "inherit",
|
fontFamily: 'inherit',
|
||||||
outline: "none",
|
outline: 'none',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
flex: "1",
|
flex: '1',
|
||||||
backgroundColor: "$mauve4",
|
backgroundColor: '$mauve4',
|
||||||
display: "inline-flex",
|
display: 'inline-flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
borderRadius: "$sm",
|
borderRadius: '$sm',
|
||||||
p: "$2",
|
p: '$2',
|
||||||
fontSize: "$md",
|
fontSize: '$md',
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
color: "$mauve12",
|
color: '$mauve12',
|
||||||
boxShadow: `0 0 0 1px $colors$mauve8`,
|
boxShadow: `0 0 0 1px $colors$mauve8`,
|
||||||
WebkitTapHighlightColor: "rgba(0,0,0,0)",
|
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
|
||||||
"&::before": {
|
'&::before': {
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box'
|
||||||
},
|
},
|
||||||
"&::after": {
|
'&::after': {
|
||||||
boxSizing: "border-box",
|
boxSizing: 'border-box'
|
||||||
},
|
},
|
||||||
fontVariantNumeric: "tabular-nums",
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
|
||||||
"&:-webkit-autofill": {
|
'&:-webkit-autofill': {
|
||||||
boxShadow: "inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3",
|
boxShadow: 'inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3'
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:-webkit-autofill::first-line": {
|
'&:-webkit-autofill::first-line': {
|
||||||
fontFamily: "$untitled",
|
fontFamily: '$untitled',
|
||||||
color: "$mauve12",
|
color: '$mauve12'
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow: `0 0 0 1px $colors$mauve10`,
|
boxShadow: `0 0 0 1px $colors$mauve10`,
|
||||||
"&:-webkit-autofill": {
|
'&:-webkit-autofill': {
|
||||||
boxShadow: `0 0 0 1px $colors$mauve10`,
|
boxShadow: `0 0 0 1px $colors$mauve10`
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&::placeholder": {
|
'&::placeholder': {
|
||||||
color: "$mauve9",
|
color: '$mauve9'
|
||||||
},
|
},
|
||||||
"&:disabled": {
|
'&:disabled': {
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none',
|
||||||
backgroundColor: "$mauve2",
|
backgroundColor: '$mauve2',
|
||||||
color: "$mauve8",
|
color: '$mauve8',
|
||||||
cursor: "not-allowed",
|
cursor: 'not-allowed',
|
||||||
"&::placeholder": {
|
'&::placeholder': {
|
||||||
color: "$mauve7",
|
color: '$mauve7'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
ghost: {
|
ghost: {
|
||||||
boxShadow: "none",
|
boxShadow: 'none',
|
||||||
backgroundColor: "transparent",
|
backgroundColor: 'transparent',
|
||||||
"@hover": {
|
'@hover': {
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
boxShadow: "inset 0 0 0 1px $colors$mauve7",
|
boxShadow: 'inset 0 0 0 1px $colors$mauve7'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
backgroundColor: "$loContrast",
|
backgroundColor: '$loContrast',
|
||||||
boxShadow: `0 0 0 1px $colors$mauve10`,
|
boxShadow: `0 0 0 1px $colors$mauve10`
|
||||||
},
|
},
|
||||||
"&:disabled": {
|
'&:disabled': {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: 'transparent'
|
||||||
},
|
|
||||||
"&:read-only": {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
},
|
||||||
|
'&:read-only': {
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deep: {
|
deep: {
|
||||||
backgroundColor: "$deep",
|
backgroundColor: '$deep',
|
||||||
boxShadow: "none",
|
boxShadow: 'none'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
invalid: {
|
invalid: {
|
||||||
boxShadow: "inset 0 0 0 1px $colors$crimson7",
|
boxShadow: 'inset 0 0 0 1px $colors$crimson7',
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow:
|
boxShadow: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8'
|
||||||
"inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8",
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
valid: {
|
valid: {
|
||||||
boxShadow: "inset 0 0 0 1px $colors$grass7",
|
boxShadow: 'inset 0 0 0 1px $colors$grass7',
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
boxShadow:
|
boxShadow: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8'
|
||||||
"inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8",
|
}
|
||||||
},
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
cursor: {
|
cursor: {
|
||||||
default: {
|
default: {
|
||||||
cursor: "default",
|
cursor: 'default',
|
||||||
"&:focus": {
|
'&:focus': {
|
||||||
cursor: "text",
|
cursor: 'text'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
cursor: "text",
|
cursor: 'text'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
export default Textarea;
|
export default Textarea
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react'
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from 'next-themes'
|
||||||
import { Sun, Moon } from "phosphor-react";
|
import { Sun, Moon } from 'phosphor-react'
|
||||||
|
|
||||||
import Button from "./Button";
|
import Button from './Button'
|
||||||
|
|
||||||
const ThemeChanger = () => {
|
const ThemeChanger = () => {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme()
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false)
|
||||||
useEffect(() => setMounted(true), []);
|
useEffect(() => setMounted(true), [])
|
||||||
|
|
||||||
if (!mounted) return null;
|
if (!mounted) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTheme(theme && theme === "light" ? "dark" : "light");
|
setTheme(theme && theme === 'light' ? 'dark' : 'light')
|
||||||
}}
|
}}
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
marginLeft: "auto",
|
marginLeft: 'auto',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
color: "$muted",
|
color: '$muted'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{theme === "dark" ? <Sun size="15px" /> : <Moon size="15px" />}
|
{theme === 'dark' ? <Sun size="15px" /> : <Moon size="15px" />}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ThemeChanger;
|
export default ThemeChanger
|
||||||
|
|||||||
@@ -1,72 +1,69 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import { styled, keyframes } from "../stitches.config";
|
import { styled, keyframes } from '../stitches.config'
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||||
|
|
||||||
const slideUpAndFade = keyframes({
|
const slideUpAndFade = keyframes({
|
||||||
"0%": { opacity: 0, transform: "translateY(2px)" },
|
'0%': { opacity: 0, transform: 'translateY(2px)' },
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
'100%': { opacity: 1, transform: 'translateY(0)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const slideRightAndFade = keyframes({
|
const slideRightAndFade = keyframes({
|
||||||
"0%": { opacity: 0, transform: "translateX(-2px)" },
|
'0%': { opacity: 0, transform: 'translateX(-2px)' },
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
'100%': { opacity: 1, transform: 'translateX(0)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const slideDownAndFade = keyframes({
|
const slideDownAndFade = keyframes({
|
||||||
"0%": { opacity: 0, transform: "translateY(-2px)" },
|
'0%': { opacity: 0, transform: 'translateY(-2px)' },
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
'100%': { opacity: 1, transform: 'translateY(0)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const slideLeftAndFade = keyframes({
|
const slideLeftAndFade = keyframes({
|
||||||
"0%": { opacity: 0, transform: "translateX(2px)" },
|
'0%': { opacity: 0, transform: 'translateX(2px)' },
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
'100%': { opacity: 1, transform: 'translateX(0)' }
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledContent = styled(TooltipPrimitive.Content, {
|
const StyledContent = styled(TooltipPrimitive.Content, {
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
padding: "$2 $3",
|
padding: '$2 $3',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
color: "$text",
|
color: '$text',
|
||||||
backgroundColor: "$background",
|
backgroundColor: '$background',
|
||||||
boxShadow:
|
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||||
"hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
|
'@media (prefers-reduced-motion: no-preference)': {
|
||||||
"@media (prefers-reduced-motion: no-preference)": {
|
animationDuration: '400ms',
|
||||||
animationDuration: "400ms",
|
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
|
animationFillMode: 'forwards',
|
||||||
animationFillMode: "forwards",
|
willChange: 'transform, opacity',
|
||||||
willChange: "transform, opacity",
|
|
||||||
'&[data-state="delayed-open"]': {
|
'&[data-state="delayed-open"]': {
|
||||||
'&[data-side="top"]': { animationName: slideDownAndFade },
|
'&[data-side="top"]': { animationName: slideDownAndFade },
|
||||||
'&[data-side="right"]': { animationName: slideLeftAndFade },
|
'&[data-side="right"]': { animationName: slideLeftAndFade },
|
||||||
'&[data-side="bottom"]': { animationName: slideUpAndFade },
|
'&[data-side="bottom"]': { animationName: slideUpAndFade },
|
||||||
'&[data-side="left"]': { animationName: slideRightAndFade },
|
'&[data-side="left"]': { animationName: slideRightAndFade }
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"0px 0px 10px 2px rgba(0,0,0,.45), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
|
'0px 0px 10px 2px rgba(0,0,0,.45), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px'
|
||||||
},
|
},
|
||||||
".light &": {
|
'.light &': {
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"0px 0px 10px 2px rgba(0,0,0,.25), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
|
'0px 0px 10px 2px rgba(0,0,0,.25), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const StyledArrow = styled(TooltipPrimitive.Arrow, {
|
const StyledArrow = styled(TooltipPrimitive.Arrow, {
|
||||||
fill: "$background",
|
fill: '$background'
|
||||||
});
|
})
|
||||||
|
|
||||||
interface ITooltip {
|
interface ITooltip {
|
||||||
content: string;
|
content: string
|
||||||
open?: boolean;
|
open?: boolean
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tooltip: React.FC<
|
const Tooltip: React.FC<React.ComponentProps<typeof StyledContent> & ITooltip> = ({
|
||||||
React.ComponentProps<typeof StyledContent> & ITooltip
|
|
||||||
> = ({
|
|
||||||
children,
|
children,
|
||||||
content,
|
content,
|
||||||
open,
|
open,
|
||||||
@@ -79,6 +76,7 @@ const Tooltip: React.FC<
|
|||||||
open={open}
|
open={open}
|
||||||
defaultOpen={defaultOpen}
|
defaultOpen={defaultOpen}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
|
delayDuration={100}
|
||||||
>
|
>
|
||||||
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
||||||
<StyledContent side="bottom" align="center" {...rest}>
|
<StyledContent side="bottom" align="center" {...rest}>
|
||||||
@@ -86,7 +84,7 @@ const Tooltip: React.FC<
|
|||||||
<StyledArrow offset={5} width={11} height={5} />
|
<StyledArrow offset={5} width={11} height={5} />
|
||||||
</StyledContent>
|
</StyledContent>
|
||||||
</TooltipPrimitive.Root>
|
</TooltipPrimitive.Root>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Tooltip;
|
export default Tooltip
|
||||||
|
|||||||
@@ -1,213 +1,267 @@
|
|||||||
import { Play } from "phosphor-react";
|
import { Play } from 'phosphor-react'
|
||||||
import { FC, useCallback, useEffect, useMemo } from "react";
|
import { FC, useCallback, useEffect } from 'react'
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from 'valtio'
|
||||||
import state from "../../state";
|
import state from '../../state'
|
||||||
import {
|
import {
|
||||||
modifyTransaction,
|
defaultTransactionType,
|
||||||
|
getTxFields,
|
||||||
|
modifyTxState,
|
||||||
prepareState,
|
prepareState,
|
||||||
prepareTransaction,
|
prepareTransaction,
|
||||||
TransactionState,
|
SelectOption,
|
||||||
} from "../../state/transactions";
|
TransactionState
|
||||||
import { sendTransaction } from "../../state/actions";
|
} from '../../state/transactions'
|
||||||
import Box from "../Box";
|
import { sendTransaction } from '../../state/actions'
|
||||||
import Button from "../Button";
|
import Box from '../Box'
|
||||||
import Flex from "../Flex";
|
import Button from '../Button'
|
||||||
import { TxJson } from "./json";
|
import Flex from '../Flex'
|
||||||
import { TxUI } from "./ui";
|
import { TxJson } from './json'
|
||||||
import { default as _estimateFee } from "../../utils/estimateFee";
|
import { TxUI } from './ui'
|
||||||
import toast from 'react-hot-toast';
|
import { default as _estimateFee } from '../../utils/estimateFee'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { combineFlags, extractFlags, transactionFlags } from '../../state/constants/flags'
|
||||||
|
import { SetHookData, toHex } from '../../utils/setHook'
|
||||||
|
|
||||||
export interface TransactionProps {
|
export interface TransactionProps {
|
||||||
header: string;
|
header: string
|
||||||
state: TransactionState;
|
state: TransactionState
|
||||||
}
|
}
|
||||||
|
|
||||||
const Transaction: FC<TransactionProps> = ({
|
const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props }) => {
|
||||||
header,
|
const { accounts, editorSettings } = useSnapshot(state)
|
||||||
state: txState,
|
const { selectedAccount, selectedTransaction, txIsDisabled, txIsLoading, viewType, editorValue } =
|
||||||
...props
|
txState
|
||||||
}) => {
|
|
||||||
const { accounts, editorSettings } = useSnapshot(state);
|
|
||||||
const {
|
|
||||||
selectedAccount,
|
|
||||||
selectedTransaction,
|
|
||||||
txIsDisabled,
|
|
||||||
txIsLoading,
|
|
||||||
viewType,
|
|
||||||
editorSavedValue,
|
|
||||||
editorValue,
|
|
||||||
} = txState;
|
|
||||||
|
|
||||||
const setState = useCallback(
|
const setState = useCallback(
|
||||||
(pTx?: Partial<TransactionState>) => {
|
(pTx?: Partial<TransactionState>) => {
|
||||||
return modifyTransaction(header, pTx);
|
return modifyTxState(header, pTx)
|
||||||
},
|
},
|
||||||
[header]
|
[header]
|
||||||
);
|
)
|
||||||
|
|
||||||
const prepareOptions = useCallback(
|
const prepareOptions = useCallback(
|
||||||
(state: TransactionState = txState) => {
|
(state: Partial<TransactionState> = txState) => {
|
||||||
const {
|
const {
|
||||||
selectedTransaction,
|
selectedTransaction,
|
||||||
selectedDestAccount,
|
selectedDestAccount,
|
||||||
selectedAccount,
|
selectedAccount,
|
||||||
txFields,
|
txFields,
|
||||||
} = state;
|
selectedFlags,
|
||||||
|
hookParameters,
|
||||||
|
memos
|
||||||
|
} = state
|
||||||
|
|
||||||
const TransactionType = selectedTransaction?.value || null;
|
const TransactionType = selectedTransaction?.value || null
|
||||||
const Destination =
|
const Destination = selectedDestAccount?.value || txFields?.Destination
|
||||||
selectedDestAccount?.value ||
|
const Account = selectedAccount?.value || null
|
||||||
("Destination" in txFields ? null : undefined);
|
const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
|
||||||
const Account = selectedAccount?.value || null;
|
const HookParameters = Object.entries(hookParameters || {}).reduce<
|
||||||
|
SetHookData['HookParameters']
|
||||||
|
>((acc, [_, { label, value }]) => {
|
||||||
|
return acc.concat({
|
||||||
|
HookParameter: { HookParameterName: toHex(label), HookParameterValue: toHex(value) }
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
const Memos = memos
|
||||||
|
? Object.entries(memos).reduce<SetHookData['Memos']>((acc, [_, { format, data, type }]) => {
|
||||||
|
return acc?.concat({
|
||||||
|
Memo: { MemoData: toHex(data), MemoFormat: toHex(format), MemoType: toHex(type) }
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
: undefined
|
||||||
|
|
||||||
return prepareTransaction({
|
return prepareTransaction({
|
||||||
...txFields,
|
...txFields,
|
||||||
|
HookParameters,
|
||||||
|
Flags,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
Destination,
|
Destination,
|
||||||
Account,
|
Account,
|
||||||
});
|
Memos
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[txState]
|
[txState]
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const transactionType = selectedTransaction?.value;
|
const transactionType = selectedTransaction?.value
|
||||||
const account = selectedAccount?.value;
|
const account = selectedAccount?.value
|
||||||
if (!account || !transactionType || txIsLoading) {
|
if (!account || !transactionType || txIsLoading) {
|
||||||
setState({ txIsDisabled: true });
|
setState({ txIsDisabled: true })
|
||||||
} else {
|
} else {
|
||||||
setState({ txIsDisabled: false });
|
setState({ txIsDisabled: false })
|
||||||
}
|
}
|
||||||
}, [
|
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading])
|
||||||
selectedAccount?.value,
|
|
||||||
selectedTransaction?.value,
|
const getJsonString = useCallback(
|
||||||
setState,
|
(state?: Partial<TransactionState>) =>
|
||||||
txIsLoading,
|
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
|
||||||
]);
|
[editorSettings.tabSize, prepareOptions]
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveEditorState = useCallback(
|
||||||
|
(value: string = '', transactionType?: string) => {
|
||||||
|
const pTx = prepareState(value, transactionType)
|
||||||
|
if (pTx) {
|
||||||
|
pTx.editorValue = getJsonString(pTx)
|
||||||
|
return setState(pTx)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[getJsonString, setState]
|
||||||
|
)
|
||||||
|
|
||||||
const submitTest = useCallback(async () => {
|
const submitTest = useCallback(async () => {
|
||||||
let st: TransactionState | undefined;
|
let st: TransactionState | undefined
|
||||||
const tt = txState.selectedTransaction?.value;
|
const tt = txState.selectedTransaction?.value
|
||||||
if (viewType === "json") {
|
if (viewType === 'json') {
|
||||||
// save the editor state first
|
st = saveEditorState(editorValue, tt)
|
||||||
const pst = prepareState(editorValue || "", tt);
|
if (!st) return
|
||||||
if (!pst) return;
|
|
||||||
|
|
||||||
st = setState(pst);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = accounts.find(
|
const account = accounts.find(acc => acc.address === selectedAccount?.value)
|
||||||
acc => acc.address === selectedAccount?.value
|
if (txIsDisabled) return
|
||||||
);
|
|
||||||
if (txIsDisabled) return;
|
|
||||||
|
|
||||||
setState({ txIsLoading: true });
|
setState({ txIsLoading: true })
|
||||||
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
|
const logPrefix = header ? `${header.split('.')[0]}: ` : undefined
|
||||||
try {
|
try {
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw Error("Account must be selected from imported accounts!");
|
throw Error('Account must be selected from imported accounts!')
|
||||||
}
|
}
|
||||||
const options = prepareOptions(st);
|
const options = prepareOptions(st)
|
||||||
|
|
||||||
if (options.Destination === null) {
|
const fields = getTxFields(options.TransactionType)
|
||||||
throw Error("Destination account cannot be null");
|
if (fields.Destination && !options.Destination) {
|
||||||
|
throw Error('Destination account is required!')
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendTransaction(account, options, { logPrefix });
|
await sendTransaction(account, options, { logPrefix })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error)
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
state.transactionLogs.push({
|
state.transactionLogs.push({
|
||||||
type: "error",
|
type: 'error',
|
||||||
message: `${logPrefix}${error.message}`,
|
message: `${logPrefix}${error.message}`
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState({ txIsLoading: false });
|
setState({ txIsLoading: false })
|
||||||
}, [
|
}, [
|
||||||
|
txState.selectedTransaction?.value,
|
||||||
viewType,
|
viewType,
|
||||||
accounts,
|
accounts,
|
||||||
txIsDisabled,
|
txIsDisabled,
|
||||||
setState,
|
setState,
|
||||||
header,
|
header,
|
||||||
|
saveEditorState,
|
||||||
editorValue,
|
editorValue,
|
||||||
txState,
|
|
||||||
selectedAccount?.value,
|
selectedAccount?.value,
|
||||||
prepareOptions,
|
prepareOptions
|
||||||
]);
|
])
|
||||||
|
|
||||||
const resetState = useCallback(() => {
|
const resetState = useCallback(
|
||||||
modifyTransaction(header, { viewType }, { replaceState: true });
|
(transactionType: SelectOption | undefined = defaultTransactionType) => {
|
||||||
}, [header, viewType]);
|
const fields = getTxFields(transactionType?.value)
|
||||||
|
|
||||||
const jsonValue = useMemo(
|
const nwState: Partial<TransactionState> = {
|
||||||
() =>
|
viewType,
|
||||||
editorSavedValue ||
|
selectedTransaction: transactionType
|
||||||
JSON.stringify(prepareOptions?.() || {}, null, editorSettings.tabSize),
|
}
|
||||||
[editorSavedValue, editorSettings.tabSize, prepareOptions]
|
|
||||||
);
|
if (fields.Destination !== undefined) {
|
||||||
|
nwState.selectedDestAccount = null
|
||||||
|
fields.Destination = ''
|
||||||
|
} else {
|
||||||
|
fields.Destination = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) {
|
||||||
|
nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags)
|
||||||
|
fields.Flags = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
nwState.txFields = fields
|
||||||
|
const state = modifyTxState(header, nwState, { replaceState: true })
|
||||||
|
const editorValue = getJsonString(state)
|
||||||
|
return setState({ editorValue })
|
||||||
|
},
|
||||||
|
[getJsonString, header, setState, viewType]
|
||||||
|
)
|
||||||
|
|
||||||
const estimateFee = useCallback(
|
const estimateFee = useCallback(
|
||||||
async (st?: TransactionState, opts?: { silent?: boolean }) => {
|
async (st?: TransactionState, opts?: { silent?: boolean }) => {
|
||||||
const state = st || txState;
|
const state = st || txState
|
||||||
const ptx = prepareOptions(state);
|
const ptx = prepareOptions(state)
|
||||||
const account = accounts.find(
|
const account = accounts.find(acc => acc.address === state.selectedAccount?.value)
|
||||||
acc => acc.address === state.selectedAccount?.value
|
|
||||||
);
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
if (!opts?.silent) {
|
if (!opts?.silent) {
|
||||||
toast.error("Please select account from the list.")
|
toast.error('Please select account from the list.')
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
};
|
}
|
||||||
|
|
||||||
ptx.Account = account.address;
|
ptx.Account = account.address
|
||||||
ptx.Sequence = account.sequence;
|
ptx.Sequence = account.sequence
|
||||||
|
|
||||||
const res = await _estimateFee(ptx, account, opts);
|
const res = await _estimateFee(ptx, account, opts)
|
||||||
const fee = res?.base_fee;
|
const fee = res?.base_fee
|
||||||
setState({ estimatedFee: fee });
|
setState({ estimatedFee: fee })
|
||||||
return fee;
|
return fee
|
||||||
},
|
},
|
||||||
[accounts, prepareOptions, setState, txState]
|
[accounts, prepareOptions, setState, txState]
|
||||||
);
|
)
|
||||||
|
|
||||||
|
const switchToJson = useCallback(() => {
|
||||||
|
const editorValue = getJsonString()
|
||||||
|
setState({ viewType: 'json', editorValue })
|
||||||
|
}, [getJsonString, setState])
|
||||||
|
|
||||||
|
const switchToUI = useCallback(() => {
|
||||||
|
setState({ viewType: 'ui' })
|
||||||
|
}, [setState])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
|
<Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}>
|
||||||
{viewType === "json" ? (
|
{viewType === 'json' ? (
|
||||||
<TxJson
|
<TxJson
|
||||||
value={jsonValue}
|
getJsonString={getJsonString}
|
||||||
|
saveEditorState={saveEditorState}
|
||||||
header={header}
|
header={header}
|
||||||
state={txState}
|
state={txState}
|
||||||
setState={setState}
|
setState={setState}
|
||||||
estimateFee={estimateFee}
|
estimateFee={estimateFee}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TxUI state={txState} setState={setState} estimateFee={estimateFee} />
|
<TxUI
|
||||||
|
switchToJson={switchToJson}
|
||||||
|
state={txState}
|
||||||
|
resetState={resetState}
|
||||||
|
setState={setState}
|
||||||
|
estimateFee={estimateFee}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Flex
|
<Flex
|
||||||
row
|
row
|
||||||
css={{
|
css={{
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
mb: "$1",
|
mb: '$1'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (viewType === "ui") {
|
if (viewType === 'ui') {
|
||||||
setState({ editorSavedValue: null, viewType: "json" });
|
switchToJson()
|
||||||
} else setState({ viewType: "ui" });
|
} else switchToUI()
|
||||||
}}
|
}}
|
||||||
outline
|
outline
|
||||||
>
|
>
|
||||||
{viewType === "ui" ? "EDIT AS JSON" : "EXIT JSON MODE"}
|
{viewType === 'ui' ? 'EDIT AS JSON' : 'EXIT JSON MODE'}
|
||||||
</Button>
|
</Button>
|
||||||
<Flex row>
|
<Flex row>
|
||||||
<Button onClick={resetState} outline css={{ mr: "$3" }}>
|
<Button onClick={() => resetState()} outline css={{ mr: '$3' }}>
|
||||||
RESET
|
RESET
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -222,7 +276,7 @@ const Transaction: FC<TransactionProps> = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Transaction;
|
export default Transaction
|
||||||
|
|||||||
@@ -1,231 +1,197 @@
|
|||||||
import Editor, { loader, useMonaco } from "@monaco-editor/react";
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import { FC, useCallback, useEffect, useState } from "react";
|
import { useSnapshot } from 'valtio'
|
||||||
import { useTheme } from "next-themes";
|
import state, { transactionsData, TransactionState } from '../../state'
|
||||||
|
import Text from '../Text'
|
||||||
import dark from "../../theme/editor/amy.json";
|
import { Flex, Link } from '..'
|
||||||
import light from "../../theme/editor/xcode_default.json";
|
import { showAlert } from '../../state/actions/showAlert'
|
||||||
import { useSnapshot } from "valtio";
|
import { parseJSON } from '../../utils/json'
|
||||||
import state, {
|
import { extractSchemaProps } from '../../utils/schema'
|
||||||
prepareState,
|
import amountSchema from '../../content/amount-schema.json'
|
||||||
transactionsData,
|
import Monaco from '../Monaco'
|
||||||
TransactionState,
|
import type monaco from 'monaco-editor'
|
||||||
} from "../../state";
|
|
||||||
import Text from "../Text";
|
|
||||||
import Flex from "../Flex";
|
|
||||||
import { Link } from "..";
|
|
||||||
import { showAlert } from "../../state/actions/showAlert";
|
|
||||||
import { parseJSON } from "../../utils/json";
|
|
||||||
import { extractSchemaProps } from "../../utils/schema";
|
|
||||||
import amountSchema from "../../content/amount-schema.json";
|
|
||||||
|
|
||||||
loader.config({
|
|
||||||
paths: {
|
|
||||||
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface JsonProps {
|
interface JsonProps {
|
||||||
value?: string;
|
getJsonString: (st?: Partial<TransactionState>) => string
|
||||||
header?: string;
|
saveEditorState: (val?: string, tt?: string) => TransactionState | undefined
|
||||||
setState: (pTx?: Partial<TransactionState> | undefined) => void;
|
header?: string
|
||||||
state: TransactionState;
|
setState: (pTx?: Partial<TransactionState> | undefined) => void
|
||||||
estimateFee?: () => Promise<string | undefined>;
|
state: TransactionState
|
||||||
|
estimateFee?: () => Promise<string | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TxJson: FC<JsonProps> = ({
|
export const TxJson: FC<JsonProps> = ({
|
||||||
value = "",
|
getJsonString,
|
||||||
state: txState,
|
state: txState,
|
||||||
header,
|
header,
|
||||||
setState,
|
setState,
|
||||||
|
saveEditorState
|
||||||
}) => {
|
}) => {
|
||||||
const { editorSettings, accounts } = useSnapshot(state);
|
const { editorSettings, accounts } = useSnapshot(state)
|
||||||
const { editorValue = value, estimatedFee } = txState;
|
const { editorValue, estimatedFee, editorIsSaved } = txState
|
||||||
const { theme } = useTheme();
|
|
||||||
const [hasUnsaved, setHasUnsaved] = useState(false);
|
|
||||||
const [currTxType, setCurrTxType] = useState<string | undefined>(
|
const [currTxType, setCurrTxType] = useState<string | undefined>(
|
||||||
txState.selectedTransaction?.value
|
txState.selectedTransaction?.value
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState({ editorValue: value });
|
const parsed = parseJSON(editorValue)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
if (!parsed) return
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const tt = parsed.TransactionType
|
||||||
const parsed = parseJSON(editorValue);
|
const tx = transactionsData.find(t => t.TransactionType === tt)
|
||||||
if (!parsed) return;
|
if (tx) setCurrTxType(tx.TransactionType)
|
||||||
|
|
||||||
const tt = parsed.TransactionType;
|
|
||||||
const tx = transactionsData.find(t => t.TransactionType === tt);
|
|
||||||
if (tx) setCurrTxType(tx.TransactionType);
|
|
||||||
else {
|
else {
|
||||||
setCurrTxType(undefined);
|
setCurrTxType(undefined)
|
||||||
}
|
}
|
||||||
}, [editorValue]);
|
}, [editorValue])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (editorValue === value) setHasUnsaved(false);
|
|
||||||
else setHasUnsaved(true);
|
|
||||||
}, [editorValue, value]);
|
|
||||||
|
|
||||||
const saveState = (value: string, transactionType?: string) => {
|
|
||||||
const tx = prepareState(value, transactionType);
|
|
||||||
if (tx) setState(tx);
|
|
||||||
};
|
|
||||||
|
|
||||||
const discardChanges = () => {
|
const discardChanges = () => {
|
||||||
showAlert("Confirm", {
|
showAlert('Confirm', {
|
||||||
body: "Are you sure to discard these changes?",
|
body: 'Are you sure to discard these changes?',
|
||||||
confirmText: "Yes",
|
confirmText: 'Yes',
|
||||||
onConfirm: () => setState({ editorValue: value }),
|
onCancel: () => {},
|
||||||
});
|
onConfirm: () => setState({ editorValue: getJsonString() })
|
||||||
};
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const onExit = (value: string) => {
|
const onExit = (value: string) => {
|
||||||
const options = parseJSON(value);
|
const options = parseJSON(value)
|
||||||
if (options) {
|
if (options) {
|
||||||
saveState(value, currTxType);
|
saveEditorState(value, currTxType)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
showAlert("Error!", {
|
showAlert('Error!', {
|
||||||
body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
|
body: `Malformed Transaction in ${header}, would you like to discard these changes?`,
|
||||||
confirmText: "Discard",
|
confirmText: 'Discard',
|
||||||
onConfirm: () => setState({ editorValue: value }),
|
onConfirm: () => setState({ editorValue: getJsonString?.() }),
|
||||||
onCancel: () => setState({ viewType: "json", editorSavedValue: value }),
|
onCancel: () => setState({ viewType: 'json' })
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const path = `file:///${header}`;
|
|
||||||
const monaco = useMonaco();
|
|
||||||
|
|
||||||
const getSchemas = useCallback(async (): Promise<any[]> => {
|
const getSchemas = useCallback(async (): Promise<any[]> => {
|
||||||
const txObj = transactionsData.find(
|
const txObj = transactionsData.find(td => td.TransactionType === currTxType)
|
||||||
td => td.TransactionType === currTxType
|
|
||||||
);
|
|
||||||
|
|
||||||
let genericSchemaProps: any;
|
let genericSchemaProps: any
|
||||||
if (txObj) {
|
if (txObj) {
|
||||||
genericSchemaProps = extractSchemaProps(txObj);
|
genericSchemaProps = extractSchemaProps(txObj)
|
||||||
} else {
|
} else {
|
||||||
genericSchemaProps = transactionsData.reduce(
|
genericSchemaProps = transactionsData.reduce(
|
||||||
(cumm, td) => ({
|
(cumm, td) => ({
|
||||||
...cumm,
|
...cumm,
|
||||||
...extractSchemaProps(td),
|
...extractSchemaProps(td)
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
uri: "file:///main-schema.json", // id of the first schema
|
uri: 'file:///main-schema.json', // id of the first schema
|
||||||
fileMatch: ["**.json"], // associate with our model
|
fileMatch: ['**.json'], // associate with our model
|
||||||
schema: {
|
schema: {
|
||||||
title: header,
|
title: header,
|
||||||
type: "object",
|
type: 'object',
|
||||||
required: ["TransactionType", "Account"],
|
required: ['TransactionType', 'Account'],
|
||||||
properties: {
|
properties: {
|
||||||
...genericSchemaProps,
|
...genericSchemaProps,
|
||||||
TransactionType: {
|
TransactionType: {
|
||||||
title: "Transaction Type",
|
title: 'Transaction Type',
|
||||||
enum: transactionsData.map(td => td.TransactionType),
|
enum: transactionsData.map(td => td.TransactionType)
|
||||||
},
|
},
|
||||||
Account: {
|
Account: {
|
||||||
$ref: "file:///account-schema.json",
|
$ref: 'file:///account-schema.json'
|
||||||
},
|
},
|
||||||
Destination: {
|
Destination: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
$ref: "file:///account-schema.json",
|
$ref: 'file:///account-schema.json'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "string",
|
type: 'string',
|
||||||
title: "Destination Account",
|
title: 'Destination Account'
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
Amount: {
|
Amount: {
|
||||||
$ref: "file:///amount-schema.json",
|
$ref: 'file:///amount-schema.json'
|
||||||
},
|
},
|
||||||
Fee: {
|
Fee: {
|
||||||
$ref: "file:///fee-schema.json",
|
$ref: 'file:///fee-schema.json'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uri: "file:///account-schema.json",
|
uri: 'file:///account-schema.json',
|
||||||
schema: {
|
schema: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
title: "Account type",
|
title: 'Account type',
|
||||||
enum: accounts.map(acc => acc.address),
|
enum: accounts.map(acc => acc.address)
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uri: "file:///fee-schema.json",
|
uri: 'file:///fee-schema.json',
|
||||||
schema: {
|
schema: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
title: "Fee type",
|
title: 'Fee type',
|
||||||
const: estimatedFee,
|
const: estimatedFee,
|
||||||
description: estimatedFee
|
description: estimatedFee ? 'Above mentioned value is recommended base fee' : undefined
|
||||||
? "Above mentioned value is recommended base fee"
|
}
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...amountSchema,
|
...amountSchema
|
||||||
},
|
}
|
||||||
];
|
]
|
||||||
}, [accounts, currTxType, estimatedFee, header]);
|
}, [accounts, currTxType, estimatedFee, header])
|
||||||
|
|
||||||
|
const [monacoInst, setMonacoInst] = useState<typeof monaco>()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!monaco) return;
|
if (!monacoInst) return
|
||||||
getSchemas().then(schemas => {
|
getSchemas().then(schemas => {
|
||||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||||
validate: true,
|
validate: true,
|
||||||
schemas,
|
schemas
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}, [getSchemas, monaco]);
|
}, [getSchemas, monacoInst])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Monaco
|
||||||
fluid
|
rootProps={{
|
||||||
column
|
css: { height: 'calc(100% - 45px)' }
|
||||||
css={{ height: "calc(100% - 45px)", position: "relative" }}
|
}}
|
||||||
>
|
language={'json'}
|
||||||
<Editor
|
id={header}
|
||||||
className="hooks-editor"
|
height="100%"
|
||||||
language={"json"}
|
value={editorValue}
|
||||||
path={path}
|
onChange={val => setState({ editorValue: val, editorIsSaved: false })}
|
||||||
height="100%"
|
onMount={(editor, monaco) => {
|
||||||
beforeMount={monaco => {
|
editor.updateOptions({
|
||||||
monaco.editor.defineTheme("dark", dark as any);
|
minimap: { enabled: false },
|
||||||
monaco.editor.defineTheme("light", light as any);
|
glyphMargin: true,
|
||||||
}}
|
tabSize: editorSettings.tabSize,
|
||||||
value={editorValue}
|
dragAndDrop: true,
|
||||||
onChange={val => setState({ editorValue: val })}
|
fontSize: 14
|
||||||
onMount={(editor, monaco) => {
|
})
|
||||||
editor.updateOptions({
|
|
||||||
minimap: { enabled: false },
|
|
||||||
glyphMargin: true,
|
|
||||||
tabSize: editorSettings.tabSize,
|
|
||||||
dragAndDrop: true,
|
|
||||||
fontSize: 14,
|
|
||||||
});
|
|
||||||
|
|
||||||
// register onExit cb
|
setMonacoInst(monaco)
|
||||||
const model = editor.getModel();
|
// register onExit cb
|
||||||
model?.onWillDispose(() => onExit(model.getValue()));
|
const model = editor.getModel()
|
||||||
}}
|
model?.onWillDispose(() => onExit(model.getValue()))
|
||||||
theme={theme === "dark" ? "dark" : "light"}
|
}}
|
||||||
/>
|
overlay={
|
||||||
{hasUnsaved && (
|
!editorIsSaved ? (
|
||||||
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}>
|
<Flex row align="center" css={{ fontSize: '$xs', color: '$textMuted', ml: 'auto' }}>
|
||||||
This file has unsaved changes.{" "}
|
<Text muted small>
|
||||||
<Link onClick={() => saveState(editorValue, currTxType)}>save</Link>{" "}
|
This file has unsaved changes.
|
||||||
<Link onClick={discardChanges}>discard</Link>
|
</Text>
|
||||||
</Text>
|
<Link css={{ ml: '$1' }} onClick={() => saveEditorState(editorValue, currTxType)}>
|
||||||
)}
|
save
|
||||||
</Flex>
|
</Link>
|
||||||
);
|
<Link css={{ ml: '$1' }} onClick={discardChanges}>
|
||||||
};
|
discard
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,292 +1,452 @@
|
|||||||
import { FC, useCallback, useEffect, useState } from "react";
|
import { FC, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import Container from "../Container";
|
import Container from '../Container'
|
||||||
import Flex from "../Flex";
|
import Flex from '../Flex'
|
||||||
import Input from "../Input";
|
import Input from '../Input'
|
||||||
import Select from "../Select";
|
import Select from '../Select'
|
||||||
import Text from "../Text";
|
import Text from '../Text'
|
||||||
import {
|
import {
|
||||||
SelectOption,
|
SelectOption,
|
||||||
TransactionState,
|
TransactionState,
|
||||||
transactionsData,
|
transactionsOptions,
|
||||||
TxFields,
|
TxFields,
|
||||||
getTxFields,
|
getTxFields,
|
||||||
} from "../../state/transactions";
|
defaultTransactionType
|
||||||
import { useSnapshot } from "valtio";
|
} from '../../state/transactions'
|
||||||
import state from "../../state";
|
import { useSnapshot } from 'valtio'
|
||||||
import { streamState } from "../DebugStream";
|
import state from '../../state'
|
||||||
import { Button } from "..";
|
import { streamState } from '../DebugStream'
|
||||||
import Textarea from "../Textarea";
|
import { Button } from '..'
|
||||||
|
import Textarea from '../Textarea'
|
||||||
|
import { getFlags } from '../../state/constants/flags'
|
||||||
|
import { Plus, Trash } from 'phosphor-react'
|
||||||
|
import AccountSequence from '../Sequence'
|
||||||
|
|
||||||
interface UIProps {
|
interface UIProps {
|
||||||
setState: (
|
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
|
||||||
pTx?: Partial<TransactionState> | undefined
|
resetState: (tt?: SelectOption) => TransactionState | undefined
|
||||||
) => TransactionState | undefined;
|
state: TransactionState
|
||||||
state: TransactionState;
|
estimateFee?: (...arg: any) => Promise<string | undefined>
|
||||||
estimateFee?: (...arg: any) => Promise<string | undefined>;
|
switchToJson: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TxUI: FC<UIProps> = ({
|
export const TxUI: FC<UIProps> = ({
|
||||||
state: txState,
|
state: txState,
|
||||||
setState,
|
setState,
|
||||||
|
resetState,
|
||||||
estimateFee,
|
estimateFee,
|
||||||
|
switchToJson
|
||||||
}) => {
|
}) => {
|
||||||
const { accounts } = useSnapshot(state);
|
const { accounts } = useSnapshot(state)
|
||||||
const {
|
const {
|
||||||
selectedAccount,
|
selectedAccount,
|
||||||
selectedDestAccount,
|
selectedDestAccount,
|
||||||
selectedTransaction,
|
selectedTransaction,
|
||||||
txFields,
|
txFields,
|
||||||
} = txState;
|
selectedFlags,
|
||||||
|
hookParameters,
|
||||||
const transactionsOptions = transactionsData.map(tx => ({
|
memos
|
||||||
value: tx.TransactionType,
|
} = txState
|
||||||
label: tx.TransactionType,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const accountOptions: SelectOption[] = accounts.map(acc => ({
|
const accountOptions: SelectOption[] = accounts.map(acc => ({
|
||||||
label: acc.name,
|
label: acc.name,
|
||||||
value: acc.address,
|
value: acc.address
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const destAccountOptions: SelectOption[] = accounts
|
const destAccountOptions: SelectOption[] = accounts
|
||||||
.map(acc => ({
|
.map(acc => ({
|
||||||
label: acc.name,
|
label: acc.name,
|
||||||
value: acc.address,
|
value: acc.address
|
||||||
}))
|
}))
|
||||||
.filter(acc => acc.value !== selectedAccount?.value);
|
.filter(acc => acc.value !== selectedAccount?.value)
|
||||||
|
|
||||||
const [feeLoading, setFeeLoading] = useState(false);
|
const flagsOptions: SelectOption[] = Object.entries(
|
||||||
|
getFlags(selectedTransaction?.value) || {}
|
||||||
|
).map(([label, value]) => ({
|
||||||
|
label,
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
|
||||||
const resetOptions = useCallback(
|
const [feeLoading, setFeeLoading] = useState(false)
|
||||||
(tt: string) => {
|
|
||||||
const fields = getTxFields(tt);
|
|
||||||
if (!fields.Destination) setState({ selectedDestAccount: null });
|
|
||||||
return setState({ txFields: fields });
|
|
||||||
},
|
|
||||||
[setState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSetAccount = (acc: SelectOption) => {
|
const handleSetAccount = (acc: SelectOption) => {
|
||||||
setState({ selectedAccount: acc });
|
setState({ selectedAccount: acc })
|
||||||
streamState.selectedAccount = acc;
|
streamState.selectedAccount = acc
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSetField = useCallback(
|
const handleSetField = useCallback(
|
||||||
(field: keyof TxFields, value: string, opFields?: TxFields) => {
|
(field: keyof TxFields, value: string, opFields?: TxFields) => {
|
||||||
const fields = opFields || txFields;
|
const fields = opFields || txFields
|
||||||
const obj = fields[field];
|
const obj = fields[field]
|
||||||
setState({
|
setState({
|
||||||
txFields: {
|
txFields: {
|
||||||
...fields,
|
...fields,
|
||||||
[field]: typeof obj === "object" ? { ...obj, $value: value } : value,
|
[field]: typeof obj === 'object' ? { ...obj, $value: value } : value
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
[setState, txFields]
|
[setState, txFields]
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleEstimateFee = useCallback(
|
const handleEstimateFee = useCallback(
|
||||||
async (state?: TransactionState, silent?: boolean) => {
|
async (state?: TransactionState, silent?: boolean) => {
|
||||||
setFeeLoading(true);
|
setFeeLoading(true)
|
||||||
|
|
||||||
const fee = await estimateFee?.(state, { silent });
|
const fee = await estimateFee?.(state, { silent })
|
||||||
if (fee) handleSetField("Fee", fee, state?.txFields);
|
if (fee) handleSetField('Fee', fee, state?.txFields)
|
||||||
|
|
||||||
setFeeLoading(false);
|
setFeeLoading(false)
|
||||||
},
|
},
|
||||||
[estimateFee, handleSetField]
|
[estimateFee, handleSetField]
|
||||||
);
|
)
|
||||||
|
|
||||||
const handleChangeTxType = (tt: SelectOption) => {
|
const handleChangeTxType = useCallback(
|
||||||
setState({ selectedTransaction: tt });
|
(tt: SelectOption) => {
|
||||||
|
setState({ selectedTransaction: tt })
|
||||||
|
|
||||||
const newState = resetOptions(tt.value);
|
const newState = resetState(tt)
|
||||||
|
|
||||||
handleEstimateFee(newState, true);
|
handleEstimateFee(newState, true)
|
||||||
};
|
},
|
||||||
|
[handleEstimateFee, resetState, setState]
|
||||||
|
)
|
||||||
|
|
||||||
const specialFields = ["TransactionType", "Account", "Destination"];
|
// default tx
|
||||||
|
|
||||||
const otherFields = Object.keys(txFields).filter(
|
|
||||||
k => !specialFields.includes(k)
|
|
||||||
) as [keyof TxFields];
|
|
||||||
|
|
||||||
const switchToJson = () =>
|
|
||||||
setState({ editorSavedValue: null, viewType: "json" });
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const defaultOption = transactionsOptions.find(
|
if (selectedTransaction?.value) return
|
||||||
tt => tt.value === "Payment"
|
|
||||||
);
|
|
||||||
if (defaultOption) {
|
|
||||||
handleChangeTxType(defaultOption);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
if (defaultTransactionType) {
|
||||||
|
handleChangeTxType(defaultTransactionType)
|
||||||
|
}
|
||||||
|
}, [handleChangeTxType, selectedTransaction?.value])
|
||||||
|
|
||||||
|
const fields = useMemo(
|
||||||
|
() => getTxFields(selectedTransaction?.value),
|
||||||
|
[selectedTransaction?.value]
|
||||||
|
)
|
||||||
|
|
||||||
|
const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos']
|
||||||
|
if (fields.Destination !== undefined) {
|
||||||
|
richFields.push('Destination')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flagsOptions.length) {
|
||||||
|
richFields.push('Flags')
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
css={{
|
css={{
|
||||||
p: "$3 01",
|
p: '$3 01',
|
||||||
fontSize: "$sm",
|
fontSize: '$sm',
|
||||||
height: "calc(100% - 45px)",
|
height: 'calc(100% - 45px)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex column fluid css={{ height: "100%", overflowY: "auto", pr: "$1" }}>
|
<Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}>
|
||||||
<Flex
|
<TxField label="Transaction type">
|
||||||
row
|
|
||||||
fluid
|
|
||||||
css={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: "$3",
|
|
||||||
mt: "1px",
|
|
||||||
pr: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text muted css={{ mr: "$3" }}>
|
|
||||||
Transaction type:{" "}
|
|
||||||
</Text>
|
|
||||||
<Select
|
<Select
|
||||||
instanceId="transactionsType"
|
instanceId="transactionsType"
|
||||||
placeholder="Select transaction type"
|
placeholder="Select transaction type"
|
||||||
options={transactionsOptions}
|
options={transactionsOptions}
|
||||||
hideSelectedOptions
|
hideSelectedOptions
|
||||||
css={{ width: "70%" }}
|
|
||||||
value={selectedTransaction}
|
value={selectedTransaction}
|
||||||
onChange={(tt: any) => handleChangeTxType(tt)}
|
onChange={(tt: any) => handleChangeTxType(tt)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</TxField>
|
||||||
<Flex
|
<TxField label="Account">
|
||||||
row
|
|
||||||
fluid
|
|
||||||
css={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: "$3",
|
|
||||||
pr: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text muted css={{ mr: "$3" }}>
|
|
||||||
Account:{" "}
|
|
||||||
</Text>
|
|
||||||
<Select
|
<Select
|
||||||
instanceId="from-account"
|
instanceId="from-account"
|
||||||
placeholder="Select your account"
|
placeholder="Select your account"
|
||||||
css={{ width: "70%" }}
|
|
||||||
options={accountOptions}
|
options={accountOptions}
|
||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
|
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</TxField>
|
||||||
{txFields.Destination !== undefined && (
|
<TxField label="Sequence">
|
||||||
<Flex
|
<AccountSequence address={selectedAccount?.value} />
|
||||||
row
|
</TxField>
|
||||||
fluid
|
{richFields.includes('Destination') && (
|
||||||
css={{
|
<TxField label="Destination account">
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: "$3",
|
|
||||||
pr: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text muted css={{ mr: "$3" }}>
|
|
||||||
Destination account:{" "}
|
|
||||||
</Text>
|
|
||||||
<Select
|
<Select
|
||||||
instanceId="to-account"
|
instanceId="to-account"
|
||||||
placeholder="Select the destination account"
|
placeholder="Select the destination account"
|
||||||
css={{ width: "70%" }}
|
|
||||||
options={destAccountOptions}
|
options={destAccountOptions}
|
||||||
value={selectedDestAccount}
|
value={selectedDestAccount}
|
||||||
isClearable
|
isClearable
|
||||||
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
|
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</TxField>
|
||||||
|
)}
|
||||||
|
{richFields.includes('Flags') && (
|
||||||
|
<TxField label="Flags">
|
||||||
|
<Select
|
||||||
|
isClearable
|
||||||
|
instanceId="flags"
|
||||||
|
placeholder="Select flags to apply"
|
||||||
|
menuPosition="fixed"
|
||||||
|
value={selectedFlags}
|
||||||
|
isMulti
|
||||||
|
options={flagsOptions}
|
||||||
|
onChange={flags => setState({ selectedFlags: flags as any })}
|
||||||
|
closeMenuOnSelect={
|
||||||
|
selectedFlags ? selectedFlags.length >= flagsOptions.length - 1 : false
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TxField>
|
||||||
)}
|
)}
|
||||||
{otherFields.map(field => {
|
{otherFields.map(field => {
|
||||||
let _value = txFields[field];
|
let _value = txFields[field]
|
||||||
|
|
||||||
let value: string | undefined;
|
let value: string | undefined
|
||||||
if (typeof _value === "object") {
|
if (typeof _value === 'object') {
|
||||||
if (_value.$type === "json" && typeof _value.$value === "object") {
|
if (_value.$type === 'json' && typeof _value.$value === 'object') {
|
||||||
value = JSON.stringify(_value.$value, null, 2);
|
value = JSON.stringify(_value.$value, null, 2)
|
||||||
} else {
|
} else {
|
||||||
value = _value.$value.toString();
|
value = _value.$value.toString()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = _value?.toString();
|
value = _value?.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isXrp = typeof _value === "object" && _value.$type === "xrp";
|
const isXrp = typeof _value === 'object' && _value.$type === 'xrp'
|
||||||
const isJson = typeof _value === "object" && _value.$type === "json";
|
const isJson = typeof _value === 'object' && _value.$type === 'json'
|
||||||
const isFee = field === "Fee";
|
const isFee = field === 'Fee'
|
||||||
let rows = isJson
|
let rows = isJson ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined
|
||||||
? (value?.match(/\n/gm)?.length || 0) + 1
|
if (rows && rows > 5) rows = 5
|
||||||
: undefined;
|
|
||||||
if (rows && rows > 5) rows = 5;
|
|
||||||
return (
|
return (
|
||||||
<Flex column key={field} css={{ mb: "$2", pr: "1px" }}>
|
<TxField key={field} label={field + (isXrp ? ' (XRP)' : '')}>
|
||||||
<Flex
|
{isJson ? (
|
||||||
row
|
<Textarea
|
||||||
fluid
|
rows={rows}
|
||||||
css={{
|
value={value}
|
||||||
justifyContent: "flex-end",
|
spellCheck={false}
|
||||||
alignItems: "center",
|
onChange={switchToJson}
|
||||||
position: "relative",
|
css={{
|
||||||
}}
|
flex: 'inherit',
|
||||||
>
|
resize: 'vertical'
|
||||||
<Text muted css={{ mr: "$3" }}>
|
}}
|
||||||
{field + (isXrp ? " (XRP)" : "")}:{" "}
|
/>
|
||||||
</Text>
|
) : (
|
||||||
{isJson ? (
|
<Input
|
||||||
<Textarea
|
type={isFee ? 'number' : 'text'}
|
||||||
rows={rows}
|
value={value}
|
||||||
value={value}
|
onChange={e => {
|
||||||
spellCheck={false}
|
if (isFee) {
|
||||||
onChange={switchToJson}
|
const val = e.target.value.replaceAll('.', '').replaceAll(',', '')
|
||||||
css={{
|
handleSetField(field, val)
|
||||||
width: "70%",
|
} else {
|
||||||
flex: "inherit",
|
handleSetField(field, e.target.value)
|
||||||
resize: "vertical",
|
}
|
||||||
|
}}
|
||||||
|
onKeyPress={
|
||||||
|
isFee
|
||||||
|
? e => {
|
||||||
|
if (e.key === '.' || e.key === ',') {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
css={{
|
||||||
|
flex: 'inherit',
|
||||||
|
'-moz-appearance': 'textfield',
|
||||||
|
'&::-webkit-outer-spin-button': {
|
||||||
|
'-webkit-appearance': 'none',
|
||||||
|
margin: 0
|
||||||
|
},
|
||||||
|
'&::-webkit-inner-spin-button ': {
|
||||||
|
'-webkit-appearance': 'none',
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isFee && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="primary"
|
||||||
|
outline
|
||||||
|
disabled={txState.txIsDisabled}
|
||||||
|
isDisabled={txState.txIsDisabled}
|
||||||
|
isLoading={feeLoading}
|
||||||
|
css={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '$2',
|
||||||
|
fontSize: '$xs',
|
||||||
|
cursor: 'pointer',
|
||||||
|
alignContent: 'center',
|
||||||
|
display: 'flex'
|
||||||
|
}}
|
||||||
|
onClick={() => handleEstimateFee()}
|
||||||
|
>
|
||||||
|
Suggest
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TxField>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<TxField multiLine label="Hook parameters">
|
||||||
|
<Flex column fluid>
|
||||||
|
{Object.entries(hookParameters).map(([id, { label, value }]) => (
|
||||||
|
<Flex column key={id} css={{ mb: '$2' }}>
|
||||||
|
<Flex row>
|
||||||
|
<Input
|
||||||
|
placeholder="Parameter name"
|
||||||
|
value={label}
|
||||||
|
onChange={e => {
|
||||||
|
setState({
|
||||||
|
hookParameters: {
|
||||||
|
...hookParameters,
|
||||||
|
[id]: { label: e.target.value, value }
|
||||||
|
}
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<Input
|
<Input
|
||||||
|
css={{ mx: '$2' }}
|
||||||
|
placeholder="Value"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
handleSetField(field, e.target.value);
|
setState({
|
||||||
}}
|
hookParameters: {
|
||||||
css={{
|
...hookParameters,
|
||||||
width: "70%",
|
[id]: { label, value: e.target.value }
|
||||||
flex: "inherit",
|
}
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{isFee && (
|
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
onClick={() => {
|
||||||
variant="primary"
|
const { [id]: _, ...rest } = hookParameters
|
||||||
outline
|
setState({ hookParameters: rest })
|
||||||
isLoading={feeLoading}
|
|
||||||
css={{
|
|
||||||
position: "absolute",
|
|
||||||
right: "$2",
|
|
||||||
fontSize: "$xs",
|
|
||||||
cursor: "pointer",
|
|
||||||
alignContent: "center",
|
|
||||||
display: "flex",
|
|
||||||
}}
|
}}
|
||||||
onClick={() => handleEstimateFee()}
|
variant="destroy"
|
||||||
>
|
>
|
||||||
Suggest
|
<Trash weight="regular" size="16px" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
))}
|
||||||
);
|
<Button
|
||||||
})}
|
outline
|
||||||
|
fullWidth
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const id = Object.keys(hookParameters).length
|
||||||
|
setState({
|
||||||
|
hookParameters: { ...hookParameters, [id]: { label: '', value: '' } }
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size="16px" />
|
||||||
|
Add Hook Parameter
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</TxField>
|
||||||
|
<TxField multiLine label="Memos">
|
||||||
|
<Flex column fluid>
|
||||||
|
{Object.entries(memos).map(([id, memo]) => (
|
||||||
|
<Flex column key={id} css={{ mb: '$2' }}>
|
||||||
|
<Flex
|
||||||
|
row
|
||||||
|
css={{
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Memo type"
|
||||||
|
value={memo.type}
|
||||||
|
onChange={e => {
|
||||||
|
setState({
|
||||||
|
memos: {
|
||||||
|
...memos,
|
||||||
|
[id]: { ...memo, type: e.target.value }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Data"
|
||||||
|
css={{ mx: '$2' }}
|
||||||
|
value={memo.data}
|
||||||
|
onChange={e => {
|
||||||
|
setState({
|
||||||
|
memos: {
|
||||||
|
...memos,
|
||||||
|
[id]: { ...memo, data: e.target.value }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Format"
|
||||||
|
value={memo.format}
|
||||||
|
onChange={e => {
|
||||||
|
setState({
|
||||||
|
memos: {
|
||||||
|
...memos,
|
||||||
|
[id]: { ...memo, format: e.target.value }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
css={{ ml: '$2' }}
|
||||||
|
onClick={() => {
|
||||||
|
const { [id]: _, ...rest } = memos
|
||||||
|
setState({ memos: rest })
|
||||||
|
}}
|
||||||
|
variant="destroy"
|
||||||
|
>
|
||||||
|
<Trash weight="regular" size="16px" />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
outline
|
||||||
|
fullWidth
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const id = Object.keys(memos).length
|
||||||
|
setState({
|
||||||
|
memos: { ...memos, [id]: { data: '', format: '', type: '' } }
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size="16px" />
|
||||||
|
Add Memo
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</TxField>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export const TxField: FC<{ label: string; children: ReactNode; multiLine?: boolean }> = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
multiLine = false
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
row
|
||||||
|
fluid
|
||||||
|
css={{
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: multiLine ? 'flex-start' : 'center',
|
||||||
|
position: 'relative',
|
||||||
|
mb: '$3',
|
||||||
|
mt: '1px',
|
||||||
|
pr: '1px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text muted css={{ mr: '$3', mt: multiLine ? '$2' : 0 }}>
|
||||||
|
{label}:{' '}
|
||||||
|
</Text>
|
||||||
|
<Flex css={{ width: '70%', alignItems: 'center' }}>{children}</Flex>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
const Carbon = () => (
|
const Carbon = () => (
|
||||||
<svg
|
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
width="66"
|
|
||||||
height="32"
|
|
||||||
viewBox="0 0 66 32"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M33 2L23 15H28L21 24H45L38 15H43L33 2Z"
|
d="M33 2L23 15H28L21 24H45L38 15H43L33 2Z"
|
||||||
stroke="#EDEDEF"
|
stroke="#EDEDEF"
|
||||||
@@ -35,6 +29,6 @@ const Carbon = () => (
|
|||||||
fill="#EDEDEF"
|
fill="#EDEDEF"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default Carbon;
|
export default Carbon
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
const Firewall = () => (
|
const Firewall = () => (
|
||||||
<svg
|
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
width="66"
|
|
||||||
height="32"
|
|
||||||
viewBox="0 0 66 32"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M33 13V7"
|
d="M33 13V7"
|
||||||
stroke="#EDEDEF"
|
stroke="#EDEDEF"
|
||||||
@@ -70,6 +64,6 @@ const Firewall = () => (
|
|||||||
fill="#EDEDEF"
|
fill="#EDEDEF"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default Firewall;
|
export default Firewall
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
const Notary = () => (
|
const Notary = () => (
|
||||||
<svg
|
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
width="66"
|
|
||||||
height="32"
|
|
||||||
viewBox="0 0 66 32"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M37.5 10.5L26.5 21.5L21 16.0002"
|
d="M37.5 10.5L26.5 21.5L21 16.0002"
|
||||||
stroke="#EDEDEF"
|
stroke="#EDEDEF"
|
||||||
@@ -35,6 +29,6 @@ const Notary = () => (
|
|||||||
fill="#EDEDEF"
|
fill="#EDEDEF"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default Notary;
|
export default Notary
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
const Peggy = () => (
|
const Peggy = () => (
|
||||||
<svg
|
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
width="66"
|
|
||||||
height="32"
|
|
||||||
viewBox="0 0 66 32"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M33 19C40.1797 19 46 16.3137 46 13C46 9.68629 40.1797 7 33 7C25.8203 7 20 9.68629 20 13C20 16.3137 25.8203 19 33 19Z"
|
d="M33 19C40.1797 19 46 16.3137 46 13C46 9.68629 40.1797 7 33 7C25.8203 7 20 9.68629 20 13C20 16.3137 25.8203 19 33 19Z"
|
||||||
stroke="#EDEDEF"
|
stroke="#EDEDEF"
|
||||||
@@ -56,6 +50,6 @@ const Peggy = () => (
|
|||||||
fill="#EDEDEF"
|
fill="#EDEDEF"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default Peggy;
|
export default Peggy
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
const Starter = () => (
|
const Starter = () => (
|
||||||
<svg
|
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
width="66"
|
|
||||||
height="32"
|
|
||||||
viewBox="0 0 66 32"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M42 28H24C23.7347 28 23.4804 27.8946 23.2929 27.7071C23.1053 27.5196 23 27.2652 23 27V5C23 4.73479 23.1053 4.48044 23.2929 4.2929C23.4804 4.10537 23.7347 4.00001 24 4H36.0003L43 11V27C43 27.2652 42.8947 27.5196 42.7071 27.7071C42.5196 27.8946 42.2653 28 42 28V28Z"
|
d="M42 28H24C23.7347 28 23.4804 27.8946 23.2929 27.7071C23.1053 27.5196 23 27.2652 23 27V5C23 4.73479 23.1053 4.48044 23.2929 4.2929C23.4804 4.10537 23.7347 4.00001 24 4H36.0003L43 11V27C43 27.2652 42.8947 27.5196 42.7071 27.7071C42.5196 27.8946 42.2653 28 42 28V28Z"
|
||||||
stroke="#EDEDEF"
|
stroke="#EDEDEF"
|
||||||
@@ -35,6 +29,6 @@ const Starter = () => (
|
|||||||
fill="#EDEDEF"
|
fill="#EDEDEF"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default Starter;
|
export default Starter
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
export { default as Flex } from "./Flex";
|
export { default as Flex } from './Flex'
|
||||||
export { default as Link } from "./Link";
|
export { default as Link } from './Link'
|
||||||
export { default as Container } from "./Container";
|
export { default as Container } from './Container'
|
||||||
export { default as Heading } from "./Heading";
|
export { default as Heading } from './Heading'
|
||||||
export { default as Stack } from "./Stack";
|
export { default as Stack } from './Stack'
|
||||||
export { default as Text } from "./Text";
|
export { default as Text } from './Text'
|
||||||
export { default as Input, Label } from "./Input";
|
export { default as Input, Label } from './Input'
|
||||||
export { default as Select } from "./Select";
|
export { default as Select } from './Select'
|
||||||
export * from "./Tabs";
|
export * from './Tabs'
|
||||||
export * from "./AlertDialog/primitive";
|
export * from './AlertDialog/primitive'
|
||||||
export { default as Box } from "./Box";
|
export { default as Box } from './Box'
|
||||||
export { default as Button } from "./Button";
|
export { default as Button } from './Button'
|
||||||
export { default as Pre } from "./Pre";
|
export { default as Pre } from './Pre'
|
||||||
export { default as ButtonGroup } from "./ButtonGroup";
|
export { default as ButtonGroup } from './ButtonGroup'
|
||||||
export * from "./Dialog";
|
export * from './Dialog'
|
||||||
export * from "./DropdownMenu";
|
export * from './DropdownMenu'
|
||||||
|
|||||||
@@ -5,10 +5,7 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": [
|
"type": ["number", "string"],
|
||||||
"number",
|
|
||||||
"string"
|
|
||||||
],
|
|
||||||
"exclusiveMinimum": 0,
|
"exclusiveMinimum": 0,
|
||||||
"maximum": "100000000000000000"
|
"maximum": "100000000000000000"
|
||||||
},
|
},
|
||||||
@@ -19,10 +16,7 @@
|
|||||||
"description": "Arbitrary currency code for the token. Cannot be XRP."
|
"description": "Arbitrary currency code for the token. Cannot be XRP."
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"type": [
|
"type": ["string", "number"],
|
||||||
"string",
|
|
||||||
"number"
|
|
||||||
],
|
|
||||||
"description": "Quoted decimal representation of the amount of the token."
|
"description": "Quoted decimal representation of the amount of the token."
|
||||||
},
|
},
|
||||||
"issuer": {
|
"issuer": {
|
||||||
@@ -40,11 +34,11 @@
|
|||||||
{
|
{
|
||||||
"label": "Token",
|
"label": "Token",
|
||||||
"body": {
|
"body": {
|
||||||
"currency": "${1:13.1}",
|
"currency": "${1:USD}",
|
||||||
"value": "${2:FOO}",
|
"value": "${2:100}",
|
||||||
"description": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
|
"issuer": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
409
content/hook-set-codes.json
Normal file
409
content/hook-set-codes.json
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": 1,
|
||||||
|
"identifier": "AMENDMENT_DISABLED",
|
||||||
|
"description": "attempt to HookSet when amendment is not yet enabled."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 2,
|
||||||
|
"identifier": "API_ILLEGAL",
|
||||||
|
"description": "HookSet object contained HookApiVersion for existing HookDefinition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 3,
|
||||||
|
"identifier": "API_INVALID",
|
||||||
|
"description": "HookSet object contained HookApiVersion for unrecognised hook API "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 4,
|
||||||
|
"identifier": "API_MISSING",
|
||||||
|
"description": "HookSet object lacked HookApiVersion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 5,
|
||||||
|
"identifier": "BLOCK_ILLEGAL",
|
||||||
|
"description": " a block end instruction moves execution below depth 0 {{}}`}` <= like this"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6,
|
||||||
|
"identifier": "CALL_ILLEGAL",
|
||||||
|
"description": "wasm tries to call a non-whitelisted function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 7,
|
||||||
|
"identifier": "CALL_INDIRECT",
|
||||||
|
"description": "wasm used call indirect instruction which is disallowed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 8,
|
||||||
|
"identifier": "CREATE_FLAG",
|
||||||
|
"description": "create operation requires hsoOVERRIDE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 9,
|
||||||
|
"identifier": "DELETE_FIELD",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 10,
|
||||||
|
"identifier": "DELETE_FLAG",
|
||||||
|
"description": "delete operation requires hsoOVERRIDE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 11,
|
||||||
|
"identifier": "DELETE_NOTHING",
|
||||||
|
"description": "delete operation would delete nothing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 12,
|
||||||
|
"identifier": "EXPORTS_MISSING",
|
||||||
|
"description": "hook did not export *any* functions (should be cbak, hook)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 13,
|
||||||
|
"identifier": "EXPORT_CBAK_FUNC",
|
||||||
|
"description": "hook did not export correct func def int64_t cbak(uint32_t)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 14,
|
||||||
|
"identifier": "EXPORT_HOOK_FUNC",
|
||||||
|
"description": "hook did not export correct func def int64_t hook(uint32_t)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 15,
|
||||||
|
"identifier": "EXPORT_MISSING",
|
||||||
|
"description": "distinct from export*S*_missing, either hook or cbak is missing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "FLAGS_INVALID",
|
||||||
|
"code": 16,
|
||||||
|
"description": "HookSet flags were invalid for specified operation "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "FUNCS_MISSING",
|
||||||
|
"code": 17,
|
||||||
|
"description": "hook did not include function code for any functions "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "FUNC_PARAM_INVALID",
|
||||||
|
"code": 18,
|
||||||
|
"description": "parameter types may only be i32 i64 u32 u64 "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "FUNC_RETURN_COUNT",
|
||||||
|
"code": 19,
|
||||||
|
"description": "a function type is defined in the wasm which returns > 1 return value "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "FUNC_RETURN_INVALID",
|
||||||
|
"code": 20,
|
||||||
|
"description": "a function type does not return i32 i64 u32 or u64 "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "FUNC_TYPELESS",
|
||||||
|
"code": 21,
|
||||||
|
"description": "hook defined hook/cbak but their type is not defined in wasm "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "FUNC_TYPE_INVALID",
|
||||||
|
"code": 22,
|
||||||
|
"description": "malformed and illegal wasm in the func type section "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "GRANTS_EMPTY",
|
||||||
|
"code": 23,
|
||||||
|
"description": "HookSet object contained an empty grants array (you should remove it) "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "GRANTS_EXCESS",
|
||||||
|
"code": 24,
|
||||||
|
"description": "HookSet object cotnained a grants array with too many grants "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "GRANTS_FIELD",
|
||||||
|
"code": 25,
|
||||||
|
"description": "HookSet object contained a grant without Authorize or HookHash "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "GRANTS_ILLEGAL",
|
||||||
|
"code": 26,
|
||||||
|
"description": "Hookset object contained grants array which contained a non Grant object "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "GUARD_IMPORT",
|
||||||
|
"code": 27,
|
||||||
|
"description": "guard import is missing "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "GUARD_MISSING",
|
||||||
|
"code": 28,
|
||||||
|
"description": "guard call missing at top of loop "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "GUARD_PARAMETERS",
|
||||||
|
"code": 29,
|
||||||
|
"description": "guard called but did not use constant expressions for params "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HASH_OR_CODE",
|
||||||
|
"code": 30,
|
||||||
|
"description": "HookSet object can contain only one of CreateCode and HookHash "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOKON_MISSING",
|
||||||
|
"code": 31,
|
||||||
|
"description": "HookSet object did not contain HookOn but should have "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOKS_ARRAY_BAD",
|
||||||
|
"code": 32,
|
||||||
|
"description": "attempt to HookSet with a Hooks array containing a non-Hook obj "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOKS_ARRAY_BLANK",
|
||||||
|
"code": 33,
|
||||||
|
"description": "all hook set objs were blank "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOKS_ARRAY_EMPTY",
|
||||||
|
"code": 34,
|
||||||
|
"description": "attempt to HookSet with an empty Hooks array "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOKS_ARRAY_MISSING",
|
||||||
|
"code": 35,
|
||||||
|
"description": "attempt to HookSet without a Hooks array "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOKS_ARRAY_TOO_BIG",
|
||||||
|
"code": 36,
|
||||||
|
"description": "attempt to HookSet with a Hooks array beyond the chain size limit "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOK_ADD",
|
||||||
|
"code": 37,
|
||||||
|
"description": "Informational: adding ltHook to directory "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOK_DEF_MISSING",
|
||||||
|
"code": 38,
|
||||||
|
"description": "attempt to reference a hook definition (by hash) that is not on ledger "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOK_DELETE",
|
||||||
|
"code": 39,
|
||||||
|
"description": "unable to delete ltHook from owner "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOK_INVALID_FIELD",
|
||||||
|
"code": 40,
|
||||||
|
"description": "HookSetObj contained an illegal/unexpected field "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOK_PARAMS_COUNT",
|
||||||
|
"code": 41,
|
||||||
|
"description": "hookset obj would create too many hook parameters "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOK_PARAM_SIZE",
|
||||||
|
"code": 42,
|
||||||
|
"description": "hookset obj sets a parameter or value that exceeds max allowable size "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "IMPORTS_MISSING",
|
||||||
|
"code": 43,
|
||||||
|
"description": "hook must import guard, and accept/rollback "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "IMPORT_ILLEGAL",
|
||||||
|
"code": 44,
|
||||||
|
"description": "attempted import of a non-whitelisted function "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "IMPORT_MODULE_BAD",
|
||||||
|
"code": 45,
|
||||||
|
"description": "hook attempted to specify no or a bad import module "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "IMPORT_MODULE_ENV",
|
||||||
|
"code": 46,
|
||||||
|
"description": "hook attempted to specify import module not named env "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "IMPORT_NAME_BAD",
|
||||||
|
"code": 47,
|
||||||
|
"description": "import name was too short or too long "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "INSTALL_FLAG",
|
||||||
|
"code": 48,
|
||||||
|
"description": "install operation requires hsoOVERRIDE "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "INSTALL_MISSING",
|
||||||
|
"code": 49,
|
||||||
|
"description": "install operation specifies hookhash which doesn't exist on the ledger "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "INSTRUCTION_COUNT",
|
||||||
|
"code": 50,
|
||||||
|
"description": "worst case execution instruction count as computed by HookSet "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "INSTRUCTION_EXCESS",
|
||||||
|
"code": 51,
|
||||||
|
"description": "worst case execution instruction count was too large "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "MEMORY_GROW",
|
||||||
|
"code": 52,
|
||||||
|
"description": "memory.grow instruction is present but disallowed "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NAMESPACE_MISSING",
|
||||||
|
"code": 53,
|
||||||
|
"description": "HookSet object lacked HookNamespace "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE",
|
||||||
|
"code": 54,
|
||||||
|
"description": "Informational: a namespace is being deleted "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_ACCOUNT",
|
||||||
|
"code": 55,
|
||||||
|
"description": "nsdelete tried to delete ns from a non-existing account "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_COUNT",
|
||||||
|
"code": 56,
|
||||||
|
"description": "namespace state count less than 0 / overflow "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_DIR",
|
||||||
|
"code": 57,
|
||||||
|
"description": "could not delete directory node in ledger "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_DIRECTORY",
|
||||||
|
"code": 58,
|
||||||
|
"description": "nsdelete operation failed to delete ns directory "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_DIR_ENTRY",
|
||||||
|
"code": 59,
|
||||||
|
"description": "nsdelete operation failed due to bad entry in ns directory "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_ENTRY",
|
||||||
|
"code": 60,
|
||||||
|
"description": "nsdelete operation failed due to missing hook state entry "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_FIELD",
|
||||||
|
"code": 61
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_FLAGS",
|
||||||
|
"code": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_NONSTATE",
|
||||||
|
"code": 63,
|
||||||
|
"description": "nsdelete operation failed due to the presence of a non-hookstate obj "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "NSDELETE_NOTHING",
|
||||||
|
"code": 64,
|
||||||
|
"description": "hsfNSDELETE provided but nothing to delete "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "OPERATION_INVALID",
|
||||||
|
"code": 65,
|
||||||
|
"description": "could not deduce an operation from the provided hookset obj "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "OVERRIDE_MISSING",
|
||||||
|
"code": 66,
|
||||||
|
"description": "HookSet object was trying to update or delete a hook but lacked hsfOVERRIDE "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "PARAMETERS_FIELD",
|
||||||
|
"code": 67,
|
||||||
|
"description": "HookParameters contained a HookParameter with an invalid key in it "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "PARAMETERS_ILLEGAL",
|
||||||
|
"code": 68,
|
||||||
|
"description": "HookParameters contained something other than a HookParameter "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "PARAMETERS_NAME",
|
||||||
|
"code": 69,
|
||||||
|
"description": "HookParameters contained a HookParameter which lacked ParameterName field "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "PARAM_HOOK_CBAK",
|
||||||
|
"code": 70,
|
||||||
|
"description": "hook and cbak must take exactly one u32 parameter "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "RETURN_HOOK_CBAK",
|
||||||
|
"code": 71,
|
||||||
|
"description": "hook and cbak must retunr i64 "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "SHORT_HOOK",
|
||||||
|
"code": 72,
|
||||||
|
"description": "web assembly byte code ended abruptly "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "TYPE_INVALID",
|
||||||
|
"code": 73,
|
||||||
|
"description": "malformed and illegal wasm specifying an illegal local var type "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "WASM_BAD_MAGIC",
|
||||||
|
"code": 74,
|
||||||
|
"description": "wasm magic number missing or not wasm "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "WASM_INVALID",
|
||||||
|
"code": 75,
|
||||||
|
"description": "set hook operation would set invalid wasm "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "WASM_PARSE_LOOP",
|
||||||
|
"code": 76,
|
||||||
|
"description": "wasm section parsing resulted in an infinite loop "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "WASM_SMOKE_TEST",
|
||||||
|
"code": 77,
|
||||||
|
"description": "Informational: first attempt to load wasm into wasm runtime "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "WASM_TEST_FAILURE",
|
||||||
|
"code": 78,
|
||||||
|
"description": "the smoke test failed "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "WASM_TOO_BIG",
|
||||||
|
"code": 79,
|
||||||
|
"description": "set hook would exceed maximum hook size "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "WASM_TOO_SMALL",
|
||||||
|
"code": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "WASM_VALIDATION",
|
||||||
|
"code": 81,
|
||||||
|
"description": "a generic error while parsing wasm, usually leb128 overflow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "HOOK_CBAK_DIFF_TYPES",
|
||||||
|
"code": 82,
|
||||||
|
"description": "hook and cbak function definitions were different"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"DestinationTag": 13,
|
"DestinationTag": 13,
|
||||||
"Fee": "2000000",
|
"Fee": "2000000",
|
||||||
"Sequence": 2470665,
|
"Sequence": 2470665,
|
||||||
"Flags": 2147483648
|
"Flags": "2147483648"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "AccountSet",
|
"TransactionType": "AccountSet",
|
||||||
@@ -48,14 +48,15 @@
|
|||||||
"Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
|
"Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
|
||||||
"Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
|
"Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
|
||||||
"Fee": "10",
|
"Fee": "10",
|
||||||
"Flags": 2147483648,
|
"Flags": "2147483648",
|
||||||
"Sequence": 2
|
"Sequence": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"TransactionType": "EscrowCancel",
|
"TransactionType": "EscrowCancel",
|
||||||
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"OfferSequence": 7
|
"OfferSequence": 7,
|
||||||
|
"Fee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
@@ -69,7 +70,8 @@
|
|||||||
"FinishAfter": 533171558,
|
"FinishAfter": 533171558,
|
||||||
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
|
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
|
||||||
"DestinationTag": 23480,
|
"DestinationTag": 23480,
|
||||||
"SourceTag": 11747
|
"SourceTag": 11747,
|
||||||
|
"Fee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
@@ -77,38 +79,59 @@
|
|||||||
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"OfferSequence": 7,
|
"OfferSequence": 7,
|
||||||
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
|
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
|
||||||
"Fulfillment": "A0028000"
|
"Fulfillment": "A0028000",
|
||||||
|
"Fee": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TransactionType": "NFTokenMint",
|
||||||
|
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
|
||||||
|
"Fee": "10",
|
||||||
|
"NFTokenTaxon": 0,
|
||||||
|
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "NFTokenBurn",
|
"TransactionType": "NFTokenBurn",
|
||||||
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
|
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
|
||||||
"Fee": "10",
|
"Fee": "10",
|
||||||
"TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
|
"NFTokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "NFTokenAcceptOffer",
|
"TransactionType": "NFTokenAcceptOffer",
|
||||||
"Fee": "10"
|
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
|
||||||
|
"Fee": "10",
|
||||||
|
"NFTokenSellOffer": "A2FA1A9911FE2AEF83DAB05F437768E26A301EF899BD31EB85E704B3D528FF18",
|
||||||
|
"NFTokenBuyOffer": "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862",
|
||||||
|
"NFTokenBrokerFee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "NFTokenCancelOffer",
|
"TransactionType": "NFTokenCancelOffer",
|
||||||
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
||||||
"TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007"
|
"Fee": "10",
|
||||||
|
"NFTokenOffers": {
|
||||||
|
"$type": "json",
|
||||||
|
"$value": [
|
||||||
|
"4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "NFTokenCreateOffer",
|
"TransactionType": "NFTokenCreateOffer",
|
||||||
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
|
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
|
||||||
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
|
"NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"$value": "100",
|
"$value": "100",
|
||||||
"$type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Flags": 1
|
"Flags": "1",
|
||||||
|
"Destination": "",
|
||||||
|
"Fee": "10",
|
||||||
|
"Owner": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TransactionType": "OfferCancel",
|
"TransactionType": "OfferCancel",
|
||||||
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
||||||
"Fee": "12",
|
"Fee": "12",
|
||||||
"Flags": 0,
|
"Flags": "0",
|
||||||
"LastLedgerSequence": 7108629,
|
"LastLedgerSequence": 7108629,
|
||||||
"OfferSequence": 6,
|
"OfferSequence": 6,
|
||||||
"Sequence": 7
|
"Sequence": 7
|
||||||
@@ -117,7 +140,7 @@
|
|||||||
"TransactionType": "OfferCreate",
|
"TransactionType": "OfferCreate",
|
||||||
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
||||||
"Fee": "12",
|
"Fee": "12",
|
||||||
"Flags": 0,
|
"Flags": "0",
|
||||||
"LastLedgerSequence": 7108682,
|
"LastLedgerSequence": 7108682,
|
||||||
"Sequence": 8,
|
"Sequence": 8,
|
||||||
"TakerGets": "6000000",
|
"TakerGets": "6000000",
|
||||||
@@ -135,7 +158,7 @@
|
|||||||
"$type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Fee": "12",
|
"Fee": "12",
|
||||||
"Flags": 2147483648,
|
"Flags": "2147483648",
|
||||||
"Sequence": 2
|
"Sequence": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -150,7 +173,8 @@
|
|||||||
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
|
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
|
||||||
"CancelAfter": 533171558,
|
"CancelAfter": 533171558,
|
||||||
"DestinationTag": 23480,
|
"DestinationTag": 23480,
|
||||||
"SourceTag": 11747
|
"SourceTag": 11747,
|
||||||
|
"Fee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
@@ -160,17 +184,18 @@
|
|||||||
"$value": "200",
|
"$value": "200",
|
||||||
"$type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Expiration": 543171558
|
"Expiration": 543171558,
|
||||||
|
"Fee": "10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Flags": 0,
|
"Flags": "0",
|
||||||
"TransactionType": "SetRegularKey",
|
"TransactionType": "SetRegularKey",
|
||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"Fee": "12",
|
"Fee": "12",
|
||||||
"RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
|
"RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Flags": 0,
|
"Flags": "0",
|
||||||
"TransactionType": "SignerListSet",
|
"TransactionType": "SignerListSet",
|
||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"Fee": "12",
|
"Fee": "12",
|
||||||
@@ -210,12 +235,25 @@
|
|||||||
"TransactionType": "TrustSet",
|
"TransactionType": "TrustSet",
|
||||||
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
||||||
"Fee": "12",
|
"Fee": "12",
|
||||||
"Flags": 262144,
|
"Flags": "262144",
|
||||||
"LastLedgerSequence": 8007750,
|
"LastLedgerSequence": 8007750,
|
||||||
"Amount": {
|
"LimitAmount": {
|
||||||
"$value": "100",
|
"$type": "json",
|
||||||
"$type": "xrp"
|
"$value": {
|
||||||
|
"currency": "USD",
|
||||||
|
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
|
||||||
|
"value": "100"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Sequence": 12
|
"Sequence": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TransactionType": "Invoke",
|
||||||
|
"Fee": "12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TransactionType": "UriToken",
|
||||||
|
"Fee": "12",
|
||||||
|
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
// Define general type for useWindowSize hook, which includes width and height
|
// Define general type for useWindowSize hook, which includes width and height
|
||||||
interface Size {
|
interface Size {
|
||||||
width: number | undefined;
|
width: number | undefined
|
||||||
height: number | undefined;
|
height: number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hook
|
// Hook
|
||||||
@@ -12,25 +12,25 @@ function useWindowSize(): Size {
|
|||||||
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
|
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
|
||||||
const [windowSize, setWindowSize] = useState<Size>({
|
const [windowSize, setWindowSize] = useState<Size>({
|
||||||
width: undefined,
|
width: undefined,
|
||||||
height: undefined,
|
height: undefined
|
||||||
});
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Handler to call on window resize
|
// Handler to call on window resize
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
// Set window width/height to state
|
// Set window width/height to state
|
||||||
setWindowSize({
|
setWindowSize({
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
// Add event listener
|
// Add event listener
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener('resize', handleResize)
|
||||||
// Call handler right away so state gets updated with initial window size
|
// Call handler right away so state gets updated with initial window size
|
||||||
handleResize();
|
handleResize()
|
||||||
// Remove event listener on cleanup
|
// Remove event listener on cleanup
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
}, []); // Empty array ensures that effect is only run on mount
|
}, []) // Empty array ensures that effect is only run on mount
|
||||||
return windowSize;
|
return windowSize
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useWindowSize;
|
export default useWindowSize
|
||||||
|
|||||||
@@ -2,22 +2,19 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
images: {
|
images: {
|
||||||
domains: ["avatars.githubusercontent.com"],
|
domains: ['avatars.githubusercontent.com']
|
||||||
},
|
},
|
||||||
webpack(config, { isServer }) {
|
webpack(config, { isServer }) {
|
||||||
config.resolve.alias["vscode"] = require.resolve(
|
config.resolve.alias['vscode'] = require.resolve(
|
||||||
"@codingame/monaco-languageclient/lib/vscode-compatibility"
|
'@codingame/monaco-languageclient/lib/vscode-compatibility'
|
||||||
);
|
)
|
||||||
config.resolve.alias["handlebars"] = require.resolve(
|
|
||||||
"handlebars/dist/handlebars.js"
|
|
||||||
);
|
|
||||||
if (!isServer) {
|
if (!isServer) {
|
||||||
config.resolve.fallback.fs = false;
|
config.resolve.fallback.fs = false
|
||||||
}
|
}
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.md$/,
|
test: [/\.md$/, /hook-bundle\.js$/],
|
||||||
use: "raw-loader",
|
use: 'raw-loader'
|
||||||
});
|
})
|
||||||
return config;
|
return config
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -7,7 +7,9 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"postinstall": "patch-package"
|
"format": "prettier --write .",
|
||||||
|
"postinstall": "patch-package && yarn run postinstall-postinstall",
|
||||||
|
"postinstall-postinstall": "./node_modules/.bin/browserify -r ripple-binary-codec -r ripple-keypairs -r ripple-address-codec -r ripple-secret-codec -r ./node_modules/xrpl-accountlib/dist/index.js:xrpl-accountlib -o node_modules/xrpl-accountlib/dist/browser.hook-bundle.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codingame/monaco-jsonrpc": "^0.3.1",
|
"@codingame/monaco-jsonrpc": "^0.3.1",
|
||||||
@@ -16,8 +18,9 @@
|
|||||||
"@octokit/core": "^3.5.1",
|
"@octokit/core": "^3.5.1",
|
||||||
"@radix-ui/colors": "^0.1.7",
|
"@radix-ui/colors": "^0.1.7",
|
||||||
"@radix-ui/react-alert-dialog": "^0.1.1",
|
"@radix-ui/react-alert-dialog": "^0.1.1",
|
||||||
|
"@radix-ui/react-context-menu": "^0.1.6",
|
||||||
"@radix-ui/react-dialog": "^0.1.1",
|
"@radix-ui/react-dialog": "^0.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^0.1.1",
|
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
||||||
"@radix-ui/react-id": "^0.1.1",
|
"@radix-ui/react-id": "^0.1.1",
|
||||||
"@radix-ui/react-label": "^0.1.5",
|
"@radix-ui/react-label": "^0.1.5",
|
||||||
"@radix-ui/react-popover": "^0.1.6",
|
"@radix-ui/react-popover": "^0.1.6",
|
||||||
@@ -25,17 +28,18 @@
|
|||||||
"@radix-ui/react-tooltip": "^0.1.7",
|
"@radix-ui/react-tooltip": "^0.1.7",
|
||||||
"@stitches/react": "^1.2.8",
|
"@stitches/react": "^1.2.8",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
|
"comment-parser": "^1.3.1",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "^8.0.7",
|
"filesize": "^8.0.7",
|
||||||
"handlebars": "^4.7.7",
|
|
||||||
"javascript-time-ago": "^2.3.11",
|
"javascript-time-ago": "^2.3.11",
|
||||||
"jszip": "^3.7.1",
|
"jszip": "^3.7.1",
|
||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
"lodash.xor": "^4.5.0",
|
"lodash.xor": "^4.5.0",
|
||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.33.0",
|
||||||
"next": "^12.0.4",
|
"next": "^12.0.4",
|
||||||
"next-auth": "^4.0.0-beta.5",
|
"next-auth": "^4.10.3",
|
||||||
|
"next-plausible": "^3.2.0",
|
||||||
"next-themes": "^0.1.1",
|
"next-themes": "^0.1.1",
|
||||||
"normalize-url": "^7.0.2",
|
"normalize-url": "^7.0.2",
|
||||||
"octokit": "^1.7.0",
|
"octokit": "^1.7.0",
|
||||||
@@ -43,12 +47,14 @@
|
|||||||
"patch-package": "^6.4.7",
|
"patch-package": "^6.4.7",
|
||||||
"phosphor-react": "^1.3.1",
|
"phosphor-react": "^1.3.1",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
"re-resizable": "^6.9.1",
|
"re-resizable": "^6.9.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-hook-form": "^7.28.0",
|
"react-hook-form": "^7.28.0",
|
||||||
"react-hot-keys": "^2.7.1",
|
"react-hot-keys": "^2.7.1",
|
||||||
"react-hot-toast": "^2.1.1",
|
"react-hot-toast": "^2.1.1",
|
||||||
|
"react-markdown": "^8.0.3",
|
||||||
"react-new-window": "^0.2.1",
|
"react-new-window": "^0.2.1",
|
||||||
"react-select": "^5.2.1",
|
"react-select": "^5.2.1",
|
||||||
"react-split": "^2.0.14",
|
"react-split": "^2.0.14",
|
||||||
@@ -59,9 +65,9 @@
|
|||||||
"valtio": "^1.2.5",
|
"valtio": "^1.2.5",
|
||||||
"vscode-languageserver": "^7.0.0",
|
"vscode-languageserver": "^7.0.0",
|
||||||
"vscode-uri": "^3.0.2",
|
"vscode-uri": "^3.0.2",
|
||||||
"wabt": "1.0.16",
|
"wabt": "^1.0.30",
|
||||||
"xrpl-accountlib": "^1.3.2",
|
"xrpl-accountlib": "^1.6.1",
|
||||||
"xrpl-client": "^1.9.4"
|
"xrpl-client": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dinero.js": "^1.9.0",
|
"@types/dinero.js": "^1.9.0",
|
||||||
@@ -70,9 +76,13 @@
|
|||||||
"@types/lodash.xor": "^4.5.6",
|
"@types/lodash.xor": "^4.5.6",
|
||||||
"@types/pako": "^1.0.2",
|
"@types/pako": "^1.0.2",
|
||||||
"@types/react": "17.0.31",
|
"@types/react": "17.0.31",
|
||||||
|
"browserify": "^17.0.0",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
"eslint-config-next": "11.1.2",
|
"eslint-config-next": "11.1.2",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"typescript": "4.4.4"
|
"typescript": "4.4.4"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"ripple-binary-codec": "=1.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
158
pages/_app.tsx
158
pages/_app.tsx
@@ -1,57 +1,54 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from 'react'
|
||||||
import "../styles/globals.css";
|
import '../styles/globals.css'
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from 'next/app'
|
||||||
import Head from "next/head";
|
import Head from 'next/head'
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from 'next-auth/react'
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from 'next-themes'
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from 'react-hot-toast'
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from 'next/router'
|
||||||
import { IdProvider } from "@radix-ui/react-id";
|
import { IdProvider } from '@radix-ui/react-id'
|
||||||
|
import PlausibleProvider from 'next-plausible'
|
||||||
|
|
||||||
import { darkTheme, css } from "../stitches.config";
|
import { darkTheme, css } from '../stitches.config'
|
||||||
import Navigation from "../components/Navigation";
|
import Navigation from '../components/Navigation'
|
||||||
import { fetchFiles } from "../state/actions";
|
import { fetchFiles } from '../state/actions'
|
||||||
import state from "../state";
|
import state from '../state'
|
||||||
|
|
||||||
import TimeAgo from "javascript-time-ago";
|
import TimeAgo from 'javascript-time-ago'
|
||||||
import en from "javascript-time-ago/locale/en.json";
|
import en from 'javascript-time-ago/locale/en.json'
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from 'valtio'
|
||||||
import Alert from "../components/AlertDialog";
|
import Alert from '../components/AlertDialog'
|
||||||
|
import { Button, Flex } from '../components'
|
||||||
|
import { ChatCircleText } from 'phosphor-react'
|
||||||
|
|
||||||
TimeAgo.setDefaultLocale(en.locale);
|
TimeAgo.setDefaultLocale(en.locale)
|
||||||
TimeAgo.addLocale(en);
|
TimeAgo.addLocale(en)
|
||||||
|
|
||||||
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const slug = router.query?.slug;
|
const slug = router.query?.slug
|
||||||
const gistId = (Array.isArray(slug) && slug[0]) ?? null;
|
const gistId = (Array.isArray(slug) && slug[0]) ?? null
|
||||||
|
|
||||||
const origin = "https://xrpl-hooks-ide.vercel.app"; // TODO: Change when site is deployed
|
const origin = 'https://xrpl-hooks-ide.vercel.app' // TODO: Change when site is deployed
|
||||||
const shareImg = "/share-image.png";
|
const shareImg = '/share-image.png'
|
||||||
|
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gistId && router.isReady) {
|
if (gistId && router.isReady) {
|
||||||
fetchFiles(gistId);
|
fetchFiles(gistId)
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
!gistId &&
|
!gistId &&
|
||||||
router.isReady &&
|
router.isReady &&
|
||||||
!router.pathname.includes("/sign-in") &&
|
router.pathname.includes('/develop') &&
|
||||||
!snap.files.length &&
|
!snap.files.length &&
|
||||||
!snap.mainModalShowed
|
!snap.mainModalShowed
|
||||||
) {
|
) {
|
||||||
state.mainModalOpen = true;
|
state.mainModalOpen = true
|
||||||
state.mainModalShowed = true;
|
state.mainModalShowed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [gistId, router.isReady, router.pathname, snap.files, snap.mainModalShowed])
|
||||||
gistId,
|
|
||||||
router.isReady,
|
|
||||||
router.pathname,
|
|
||||||
snap.files,
|
|
||||||
snap.mainModalShowed,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -82,38 +79,17 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
|||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
<meta name="twitter:image" content={`${origin}${shareImg}`} />
|
<meta name="twitter:image" content={`${origin}${shareImg}`} />
|
||||||
<link
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
rel="apple-touch-icon"
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
sizes="180x180"
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
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="manifest" href="/site.webmanifest" />
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
|
||||||
<meta name="application-name" content="XRPL Hooks Builder" />
|
<meta name="application-name" content="XRPL Hooks Builder" />
|
||||||
<meta name="msapplication-TileColor" content="#c10ad0" />
|
<meta name="msapplication-TileColor" content="#c10ad0" />
|
||||||
<meta
|
<meta name="theme-color" content="#161618" media="(prefers-color-scheme: dark)" />
|
||||||
name="theme-color"
|
<meta name="theme-color" content="#FDFCFD" media="(prefers-color-scheme: light)" />
|
||||||
content="#161618"
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="theme-color"
|
|
||||||
content="#FDFCFD"
|
|
||||||
media="(prefers-color-scheme: light)"
|
|
||||||
/>
|
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<IdProvider>
|
<IdProvider>
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
@@ -121,31 +97,45 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
|||||||
defaultTheme="dark"
|
defaultTheme="dark"
|
||||||
enableSystem={false}
|
enableSystem={false}
|
||||||
value={{
|
value={{
|
||||||
light: "light",
|
light: 'light',
|
||||||
dark: darkTheme.className,
|
dark: darkTheme.className
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Navigation />
|
<PlausibleProvider domain="hooks-builder.xrpl.org" trackOutboundLinks>
|
||||||
<Component {...pageProps} />
|
<Navigation />
|
||||||
<Toaster
|
<Component {...pageProps} />
|
||||||
toastOptions={{
|
<Toaster
|
||||||
className: css({
|
toastOptions={{
|
||||||
backgroundColor: "$mauve1",
|
className: css({
|
||||||
color: "$mauve10",
|
backgroundColor: '$mauve1',
|
||||||
fontSize: "$sm",
|
color: '$mauve10',
|
||||||
zIndex: 9999,
|
fontSize: '$sm',
|
||||||
".dark &": {
|
zIndex: 9999,
|
||||||
backgroundColor: "$mauve4",
|
'.dark &': {
|
||||||
color: "$mauve12",
|
backgroundColor: '$mauve4',
|
||||||
},
|
color: '$mauve12'
|
||||||
})(),
|
}
|
||||||
}}
|
})()
|
||||||
/>
|
}}
|
||||||
<Alert />
|
/>
|
||||||
|
<Alert />
|
||||||
|
<Flex
|
||||||
|
as="a"
|
||||||
|
href="https://github.com/XRPLF/Hooks/discussions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
css={{ position: 'fixed', right: '$4', bottom: '$4' }}
|
||||||
|
>
|
||||||
|
<Button size="sm" variant="primary" outline>
|
||||||
|
<ChatCircleText size={14} style={{ marginRight: '0px' }} />
|
||||||
|
Bugs & Discussions
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</PlausibleProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</IdProvider>
|
</IdProvider>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
export default MyApp;
|
export default MyApp
|
||||||
|
|||||||
@@ -1,35 +1,22 @@
|
|||||||
import Document, {
|
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
|
||||||
Html,
|
|
||||||
Head,
|
|
||||||
Main,
|
|
||||||
NextScript,
|
|
||||||
DocumentContext,
|
|
||||||
} from "next/document";
|
|
||||||
|
|
||||||
import { globalStyles, getCssText } from "../stitches.config";
|
import { globalStyles, getCssText } from '../stitches.config'
|
||||||
|
|
||||||
class MyDocument extends Document {
|
class MyDocument extends Document {
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
static async getInitialProps(ctx: DocumentContext) {
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
const initialProps = await Document.getInitialProps(ctx)
|
||||||
|
|
||||||
return initialProps;
|
return initialProps
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
globalStyles();
|
globalStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head>
|
<Head>
|
||||||
<style
|
<style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
|
||||||
id="stitches"
|
|
||||||
dangerouslySetInnerHTML={{ __html: getCssText() }}
|
|
||||||
/>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
||||||
rel="preconnect"
|
|
||||||
href="https://fonts.gstatic.com"
|
|
||||||
crossOrigin=""
|
|
||||||
/>
|
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&family=Work+Sans:wght@400;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&family=Work+Sans:wght@400;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -40,8 +27,8 @@ class MyDocument extends Document {
|
|||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</Html>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyDocument;
|
export default MyDocument
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import type { NextRequest, NextFetchEvent } from 'next/server';
|
import type { NextRequest, NextFetchEvent } from 'next/server'
|
||||||
import { NextResponse as Response } from 'next/server';
|
import { NextResponse as Response } from 'next/server'
|
||||||
|
|
||||||
export default function middleware(req: NextRequest, ev: NextFetchEvent) {
|
export default function middleware(req: NextRequest, ev: NextFetchEvent) {
|
||||||
|
if (req.nextUrl.pathname === '/') {
|
||||||
if (req.nextUrl.pathname === "/") {
|
const url = req.nextUrl.clone()
|
||||||
const url = req.nextUrl.clone();
|
url.pathname = '/develop'
|
||||||
url.pathname = '/develop';
|
return Response.redirect(url)
|
||||||
return Response.redirect(url);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import NextAuth from "next-auth"
|
import NextAuth from 'next-auth'
|
||||||
|
|
||||||
export default NextAuth({
|
export default NextAuth({
|
||||||
// Configure one or more authentication providers
|
// Configure one or more authentication providers
|
||||||
@@ -10,39 +10,38 @@ export default NextAuth({
|
|||||||
// scope: 'user,gist'
|
// scope: 'user,gist'
|
||||||
// }),
|
// }),
|
||||||
{
|
{
|
||||||
id: "github",
|
id: 'github',
|
||||||
name: "GitHub",
|
name: 'GitHub',
|
||||||
type: "oauth",
|
type: 'oauth',
|
||||||
clientId: process.env.GITHUB_ID,
|
clientId: process.env.GITHUB_ID,
|
||||||
clientSecret: process.env.GITHUB_SECRET,
|
clientSecret: process.env.GITHUB_SECRET,
|
||||||
authorization: "https://github.com/login/oauth/authorize?scope=read:user+user:email+gist",
|
authorization: 'https://github.com/login/oauth/authorize?scope=read:user+user:email+gist',
|
||||||
token: "https://github.com/login/oauth/access_token",
|
token: 'https://github.com/login/oauth/access_token',
|
||||||
userinfo: "https://api.github.com/user",
|
userinfo: 'https://api.github.com/user',
|
||||||
profile(profile) {
|
profile(profile) {
|
||||||
return {
|
return {
|
||||||
id: profile.id.toString(),
|
id: profile.id.toString(),
|
||||||
name: profile.name || profile.login,
|
name: profile.name || profile.login,
|
||||||
username: profile.login,
|
username: profile.login,
|
||||||
email: profile.email,
|
email: profile.email,
|
||||||
image: profile.avatar_url,
|
image: profile.avatar_url
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// ...add more providers here
|
// ...add more providers here
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, user, account, profile, isNewUser }) {
|
async jwt({ token, user, account, profile, isNewUser }) {
|
||||||
if (account && account.access_token) {
|
if (account && account.access_token) {
|
||||||
token.accessToken = account.access_token;
|
token.accessToken = account.access_token
|
||||||
token.username = user?.username || '';
|
token.username = user?.username || ''
|
||||||
}
|
}
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
session.accessToken = token.accessToken as string;
|
session.accessToken = token.accessToken as string
|
||||||
session['user']['username'] = token.username as string;
|
session['user']['username'] = token.username as string
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ interface ErrorResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Faucet {
|
export interface Faucet {
|
||||||
address: string;
|
address: string
|
||||||
secret: string;
|
secret: string
|
||||||
xrp: number;
|
xrp: number
|
||||||
hash: string;
|
hash: string
|
||||||
code: string;
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<Faucet | ErrorResponse>
|
res: NextApiResponse<Faucet | ErrorResponse>
|
||||||
@@ -21,20 +20,25 @@ export default async function handler(
|
|||||||
if (req.method !== 'POST') {
|
if (req.method !== 'POST') {
|
||||||
return res.status(405).json({ error: 'Method not allowed!' })
|
return res.status(405).json({ error: 'Method not allowed!' })
|
||||||
}
|
}
|
||||||
const { account } = req.query;
|
const { account } = req.query
|
||||||
const ip = Array.isArray(req?.headers?.["x-real-ip"]) ? req?.headers?.["x-real-ip"][0] : req?.headers?.["x-real-ip"];
|
const ip = Array.isArray(req?.headers?.['x-real-ip'])
|
||||||
|
? req?.headers?.['x-real-ip'][0]
|
||||||
|
: req?.headers?.['x-real-ip']
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
`https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`,
|
||||||
headers: {
|
{
|
||||||
'x-forwarded-for': ip || '',
|
method: 'POST',
|
||||||
},
|
headers: {
|
||||||
});
|
'x-forwarded-for': ip || ''
|
||||||
const json: Faucet | ErrorResponse = await response.json();
|
}
|
||||||
if ("error" in json) {
|
}
|
||||||
|
)
|
||||||
|
const json: Faucet | ErrorResponse = await response.json()
|
||||||
|
if ('error' in json) {
|
||||||
return res.status(429).json(json)
|
return res.status(429).json(json)
|
||||||
}
|
}
|
||||||
return res.status(200).json(json);
|
return res.status(200).json(json)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return res.status(500).json({ error: 'Server error' })
|
return res.status(500).json({ error: 'Server error' })
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ type Data = {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function handler(
|
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Data>
|
|
||||||
) {
|
|
||||||
res.status(200).json({ name: 'John Doe' })
|
res.status(200).json({ name: 'John Doe' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
req: NextApiRequest,
|
try {
|
||||||
res: NextApiResponse
|
const { url, opts } = req.body
|
||||||
) {
|
const r = await fetch(url, opts)
|
||||||
try {
|
if (!r.ok) throw r.statusText
|
||||||
const { url, opts } = req.body
|
|
||||||
const r = await fetch(url, opts);
|
|
||||||
if (!r.ok) throw (r.statusText)
|
|
||||||
|
|
||||||
const data = await r.json()
|
const data = await r.json()
|
||||||
return res.json(data)
|
return res.json(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
return res.status(500).json({ message: "Something went wrong!" })
|
return res.status(500).json({ message: 'Something went wrong!' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,59 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from 'next/dynamic'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import Split from "react-split";
|
import Split from 'react-split'
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from 'valtio'
|
||||||
import state from "../../state";
|
import state from '../../state'
|
||||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
|
||||||
|
|
||||||
const DeployEditor = dynamic(() => import("../../components/DeployEditor"), {
|
const DeployEditor = dynamic(() => import('../../components/DeployEditor'), {
|
||||||
ssr: false,
|
ssr: false
|
||||||
});
|
})
|
||||||
|
|
||||||
const Accounts = dynamic(() => import("../../components/Accounts"), {
|
const Accounts = dynamic(() => import('../../components/Accounts'), {
|
||||||
ssr: false,
|
ssr: false
|
||||||
});
|
})
|
||||||
|
|
||||||
const LogBox = dynamic(() => import("../../components/LogBox"), {
|
const LogBox = dynamic(() => import('../../components/LogBox'), {
|
||||||
ssr: false,
|
ssr: false
|
||||||
});
|
})
|
||||||
|
|
||||||
const Deploy = () => {
|
const Deploy = () => {
|
||||||
const { deployLogs } = useSnapshot(state);
|
const { deployLogs } = useSnapshot(state)
|
||||||
return (
|
return (
|
||||||
<Split
|
<Split
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
gutterSize={4}
|
gutterSize={4}
|
||||||
gutterAlign="center"
|
gutterAlign="center"
|
||||||
sizes={getSplit("deployVertical") || [40, 60]}
|
sizes={getSplit('deployVertical') || [40, 60]}
|
||||||
style={{ height: "calc(100vh - 60px)" }}
|
style={{ height: 'calc(100vh - 60px)' }}
|
||||||
onDragEnd={(e) => saveSplit("deployVertical", e)}
|
onDragEnd={e => saveSplit('deployVertical', e)}
|
||||||
>
|
>
|
||||||
<main style={{ display: "flex", flex: 1, position: "relative" }}>
|
<main style={{ display: 'flex', flex: 1, position: 'relative' }}>
|
||||||
<DeployEditor />
|
<DeployEditor />
|
||||||
</main>
|
</main>
|
||||||
<Split
|
<Split
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
sizes={getSplit("deployHorizontal") || [50, 50]}
|
sizes={getSplit('deployHorizontal') || [50, 50]}
|
||||||
minSize={[320, 160]}
|
minSize={[320, 160]}
|
||||||
gutterSize={4}
|
gutterSize={4}
|
||||||
gutterAlign="center"
|
gutterAlign="center"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "row",
|
flexDirection: 'row',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
height: "100%",
|
height: '100%'
|
||||||
}}
|
}}
|
||||||
onDragEnd={(e) => saveSplit("deployHorizontal", e)}
|
onDragEnd={e => saveSplit('deployHorizontal', e)}
|
||||||
>
|
>
|
||||||
<div style={{ alignItems: "stretch", display: "flex" }}>
|
<div style={{ alignItems: 'stretch', display: 'flex' }}>
|
||||||
<Accounts />
|
<Accounts />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<LogBox
|
<LogBox title="Deploy Log" logs={deployLogs} clearLog={() => (state.deployLogs = [])} />
|
||||||
title="Deploy Log"
|
|
||||||
logs={deployLogs}
|
|
||||||
clearLog={() => (state.deployLogs = [])}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Split>
|
</Split>
|
||||||
</Split>
|
</Split>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Deploy;
|
export default Deploy
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
import { Label } from "@radix-ui/react-label";
|
import { Label } from '@radix-ui/react-label'
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from 'next'
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from 'next/dynamic'
|
||||||
import { Gear, Play } from "phosphor-react";
|
import { FileJs, Gear, Play } from 'phosphor-react'
|
||||||
import Hotkeys from "react-hot-keys";
|
import Hotkeys from 'react-hot-keys'
|
||||||
import Split from "react-split";
|
import Split from 'react-split'
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from 'valtio'
|
||||||
import { ButtonGroup, Flex } from "../../components";
|
import { ButtonGroup, Flex } from '../../components'
|
||||||
import Box from "../../components/Box";
|
import Box from '../../components/Box'
|
||||||
import Button from "../../components/Button";
|
import Button from '../../components/Button'
|
||||||
import LogBoxForScripts from "../../components/LogBoxForScripts";
|
import Popover from '../../components/Popover'
|
||||||
import Popover from "../../components/Popover";
|
import RunScript from '../../components/RunScript'
|
||||||
import RunScript from "../../components/RunScript";
|
import state, { IFile } from '../../state'
|
||||||
import state from "../../state";
|
import { compileCode } from '../../state/actions'
|
||||||
import { compileCode } from "../../state/actions";
|
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
|
||||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
import { styled } from '../../stitches.config'
|
||||||
import { styled } from "../../stitches.config";
|
import { getFileExtention } from '../../utils/helpers'
|
||||||
|
|
||||||
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
|
const HooksEditor = dynamic(() => import('../../components/HooksEditor'), {
|
||||||
ssr: false,
|
ssr: false
|
||||||
});
|
})
|
||||||
|
|
||||||
const LogBox = dynamic(() => import("../../components/LogBox"), {
|
const LogBox = dynamic(() => import('../../components/LogBox'), {
|
||||||
ssr: false,
|
ssr: false
|
||||||
});
|
})
|
||||||
|
|
||||||
const OptimizationText = () => (
|
const OptimizationText = () => (
|
||||||
<span>
|
<span>
|
||||||
Specify which optimization level to use for compiling. For example -O0 means
|
Specify which optimization level to use for compiling. For example -O0 means “no optimization”:
|
||||||
“no optimization”: this level compiles the fastest and generates the most
|
this level compiles the fastest and generates the most debuggable code. -O2 means moderate level
|
||||||
debuggable code. -O2 means moderate level of optimization which enables most
|
of optimization which enables most optimizations. Read more about the options from{' '}
|
||||||
optimizations. Read more about the options from{" "}
|
|
||||||
<a
|
<a
|
||||||
className="link"
|
className="link"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -40,144 +39,144 @@ const OptimizationText = () => (
|
|||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</span>
|
</span>
|
||||||
);
|
)
|
||||||
|
|
||||||
const StyledOptimizationText = styled(OptimizationText, {
|
const StyledOptimizationText = styled(OptimizationText, {
|
||||||
color: "$mauve12 !important",
|
color: '$mauve12 !important',
|
||||||
fontSize: "200px",
|
fontSize: '200px',
|
||||||
"span a.link": {
|
'span a.link': {
|
||||||
color: "red",
|
color: 'red'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const CompilerSettings = () => {
|
const CompilerSettings = () => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state)
|
||||||
return (
|
return (
|
||||||
<Flex css={{ minWidth: 200, flexDirection: "column", gap: "$5" }}>
|
<Flex css={{ minWidth: 200, flexDirection: 'column', gap: '$5' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Label
|
<Label
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: 'row',
|
||||||
display: "flex",
|
display: 'flex'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Optimization level{" "}
|
Optimization level{' '}
|
||||||
<Popover
|
<Popover
|
||||||
css={{
|
css={{
|
||||||
maxWidth: "240px",
|
maxWidth: '240px',
|
||||||
lineHeight: "1.3",
|
lineHeight: '1.3',
|
||||||
a: {
|
a: {
|
||||||
color: "$purple11",
|
color: '$purple11'
|
||||||
},
|
},
|
||||||
".dark &": {
|
'.dark &': {
|
||||||
backgroundColor: "$black !important",
|
backgroundColor: '$black !important',
|
||||||
|
|
||||||
".arrow": {
|
'.arrow': {
|
||||||
fill: "$colors$black",
|
fill: '$colors$black'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
content={<StyledOptimizationText />}
|
content={<StyledOptimizationText />}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
top: "-1px",
|
top: '-1px',
|
||||||
ml: "$1",
|
ml: '$1',
|
||||||
backgroundColor: "$mauve8",
|
backgroundColor: '$mauve8',
|
||||||
borderRadius: "$full",
|
borderRadius: '$full',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
width: "16px",
|
width: '16px',
|
||||||
height: "16px",
|
height: '16px',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
?
|
?
|
||||||
</Flex>
|
</Flex>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Label>
|
</Label>
|
||||||
<ButtonGroup css={{ mt: "$2", fontFamily: "$monospace" }}>
|
<ButtonGroup css={{ mt: '$2', fontFamily: '$monospace' }}>
|
||||||
<Button
|
<Button
|
||||||
css={{ fontFamily: "$monospace" }}
|
css={{ fontFamily: '$monospace' }}
|
||||||
outline={snap.compileOptions.optimizationLevel !== "-O0"}
|
outline={snap.compileOptions.optimizationLevel !== '-O0'}
|
||||||
onClick={() => (state.compileOptions.optimizationLevel = "-O0")}
|
onClick={() => (state.compileOptions.optimizationLevel = '-O0')}
|
||||||
>
|
>
|
||||||
-O0
|
-O0
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
css={{ fontFamily: "$monospace" }}
|
css={{ fontFamily: '$monospace' }}
|
||||||
outline={snap.compileOptions.optimizationLevel !== "-O1"}
|
outline={snap.compileOptions.optimizationLevel !== '-O1'}
|
||||||
onClick={() => (state.compileOptions.optimizationLevel = "-O1")}
|
onClick={() => (state.compileOptions.optimizationLevel = '-O1')}
|
||||||
>
|
>
|
||||||
-O1
|
-O1
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
css={{ fontFamily: "$monospace" }}
|
css={{ fontFamily: '$monospace' }}
|
||||||
outline={snap.compileOptions.optimizationLevel !== "-O2"}
|
outline={snap.compileOptions.optimizationLevel !== '-O2'}
|
||||||
onClick={() => (state.compileOptions.optimizationLevel = "-O2")}
|
onClick={() => (state.compileOptions.optimizationLevel = '-O2')}
|
||||||
>
|
>
|
||||||
-O2
|
-O2
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
css={{ fontFamily: "$monospace" }}
|
css={{ fontFamily: '$monospace' }}
|
||||||
outline={snap.compileOptions.optimizationLevel !== "-O3"}
|
outline={snap.compileOptions.optimizationLevel !== '-O3'}
|
||||||
onClick={() => (state.compileOptions.optimizationLevel = "-O3")}
|
onClick={() => (state.compileOptions.optimizationLevel = '-O3')}
|
||||||
>
|
>
|
||||||
-O3
|
-O3
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
css={{ fontFamily: "$monospace" }}
|
css={{ fontFamily: '$monospace' }}
|
||||||
outline={snap.compileOptions.optimizationLevel !== "-O4"}
|
outline={snap.compileOptions.optimizationLevel !== '-O4'}
|
||||||
onClick={() => (state.compileOptions.optimizationLevel = "-O4")}
|
onClick={() => (state.compileOptions.optimizationLevel = '-O4')}
|
||||||
>
|
>
|
||||||
-O4
|
-O4
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
css={{ fontFamily: "$monospace" }}
|
css={{ fontFamily: '$monospace' }}
|
||||||
outline={snap.compileOptions.optimizationLevel !== "-Os"}
|
outline={snap.compileOptions.optimizationLevel !== '-Os'}
|
||||||
onClick={() => (state.compileOptions.optimizationLevel = "-Os")}
|
onClick={() => (state.compileOptions.optimizationLevel = '-Os')}
|
||||||
>
|
>
|
||||||
-Os
|
-Os
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state)
|
||||||
|
|
||||||
|
const activeFile = snap.files[snap.active] as IFile | undefined
|
||||||
|
const activeFileExt = getFileExtention(activeFile?.name)
|
||||||
|
const canCompile = activeFileExt === 'c' || activeFileExt === 'wat'
|
||||||
return (
|
return (
|
||||||
<Split
|
<Split
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
sizes={getSplit("developVertical") || [70, 30]}
|
sizes={getSplit('developVertical') || [70, 30]}
|
||||||
minSize={[100, 100]}
|
minSize={[100, 100]}
|
||||||
gutterAlign="center"
|
gutterAlign="center"
|
||||||
gutterSize={4}
|
gutterSize={4}
|
||||||
style={{ height: "calc(100vh - 60px)" }}
|
style={{ height: 'calc(100vh - 60px)' }}
|
||||||
onDragEnd={(e) => saveSplit("developVertical", e)}
|
onDragEnd={e => saveSplit('developVertical', e)}
|
||||||
>
|
>
|
||||||
<main style={{ display: "flex", flex: 1, position: "relative" }}>
|
<main style={{ display: 'flex', flex: 1, position: 'relative' }}>
|
||||||
<HooksEditor />
|
<HooksEditor />
|
||||||
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
|
{canCompile && (
|
||||||
"c" && (
|
|
||||||
<Hotkeys
|
<Hotkeys
|
||||||
keyName="command+b,ctrl+b"
|
keyName="command+b,ctrl+b"
|
||||||
onKeyDown={() =>
|
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
|
||||||
!snap.compiling && snap.files.length && compileCode(snap.active)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
bottom: "$4",
|
bottom: '$4',
|
||||||
left: "$4",
|
left: '$4',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
gap: "$2",
|
gap: '$2'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -191,61 +190,53 @@ const Home: NextPage = () => {
|
|||||||
Compile to Wasm
|
Compile to Wasm
|
||||||
</Button>
|
</Button>
|
||||||
<Popover content={<CompilerSettings />}>
|
<Popover content={<CompilerSettings />}>
|
||||||
<Button variant="primary" css={{ px: "10px" }}>
|
<Button variant="primary" css={{ px: '10px' }}>
|
||||||
<Gear size="16px" />
|
<Gear size="16px" />
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Hotkeys>
|
</Hotkeys>
|
||||||
)}
|
)}
|
||||||
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
|
{activeFileExt === 'js' && (
|
||||||
"js" && (
|
|
||||||
<Hotkeys
|
<Hotkeys
|
||||||
keyName="command+b,ctrl+b"
|
keyName="command+b,ctrl+b"
|
||||||
onKeyDown={() =>
|
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
|
||||||
!snap.compiling && snap.files.length && compileCode(snap.active)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
bottom: "$4",
|
bottom: '$4',
|
||||||
left: "$4",
|
left: '$4',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
gap: "$2",
|
gap: '$2'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RunScript file={snap.files[snap.active]} />
|
<RunScript file={activeFile as IFile} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Hotkeys>
|
</Hotkeys>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
<Flex css={{ width: "100%" }}>
|
<Flex css={{ width: '100%' }}>
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
background: "$mauve1",
|
background: '$mauve1',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
borderRight: "1px solid $mauve8",
|
borderRight: '1px solid $mauve8'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogBox
|
<LogBox title="Development Log" clearLog={() => (state.logs = [])} logs={snap.logs} />
|
||||||
title="Development Log"
|
|
||||||
clearLog={() => (state.logs = [])}
|
|
||||||
logs={snap.logs}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
|
{activeFileExt === 'js' && (
|
||||||
"js" && (
|
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
flex: 1,
|
flex: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogBoxForScripts
|
<LogBox
|
||||||
showButtons={false}
|
Icon={FileJs}
|
||||||
title="Script Log"
|
title="Script Log"
|
||||||
logs={snap.scriptLogs}
|
logs={snap.scriptLogs}
|
||||||
clearLog={() => (state.scriptLogs = [])}
|
clearLog={() => (state.scriptLogs = [])}
|
||||||
@@ -254,7 +245,7 @@ const Home: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Split>
|
</Split>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const Home = () => {
|
const Home = () => {
|
||||||
return <p>homepage</p>;
|
return <p>homepage</p>
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home
|
||||||
|
|||||||
@@ -1,38 +1,37 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from 'react'
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from 'next-auth/react'
|
||||||
|
|
||||||
import Box from "../components/Box";
|
import Box from '../components/Box'
|
||||||
import Spinner from "../components/Spinner";
|
import Spinner from '../components/Spinner'
|
||||||
|
|
||||||
const SignInPage = () => {
|
const SignInPage = () => {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status !== "loading" && !session)
|
if (status !== 'loading' && !session) void signIn('github', { redirect: false })
|
||||||
void signIn("github", { redirect: false });
|
if (status !== 'loading' && session) window.close()
|
||||||
if (status !== "loading" && session) window.close();
|
}, [session, status])
|
||||||
}, [session, status]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
backgroundColor: "$mauve1",
|
backgroundColor: '$mauve1',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
gap: "$2",
|
gap: '$2'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Logging in <Spinner />
|
Logging in <Spinner />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default SignInPage;
|
export default SignInPage
|
||||||
|
|||||||
@@ -1,87 +1,102 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from 'next/dynamic'
|
||||||
import Split from "react-split";
|
import Split from 'react-split'
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from 'valtio'
|
||||||
import { Box, Container, Flex, Tab, Tabs } from "../../components";
|
import { Box, Container, Flex, Tab, Tabs } from '../../components'
|
||||||
import Transaction from "../../components/Transaction";
|
import Transaction from '../../components/Transaction'
|
||||||
import state from "../../state";
|
import state, { renameTxState } from '../../state'
|
||||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
|
||||||
import { transactionsState, modifyTransaction } from "../../state";
|
import { transactionsState, modifyTxState } from '../../state'
|
||||||
import LogBoxForScripts from "../../components/LogBoxForScripts";
|
import { useEffect, useState } from 'react'
|
||||||
import { useEffect, useState } from "react";
|
import { FileJs } from 'phosphor-react'
|
||||||
|
import RunScript from '../../components/RunScript'
|
||||||
|
|
||||||
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
|
const DebugStream = dynamic(() => import('../../components/DebugStream'), {
|
||||||
ssr: false,
|
ssr: false
|
||||||
});
|
})
|
||||||
|
|
||||||
const LogBox = dynamic(() => import("../../components/LogBox"), {
|
const LogBox = dynamic(() => import('../../components/LogBox'), {
|
||||||
ssr: false,
|
ssr: false
|
||||||
});
|
})
|
||||||
const Accounts = dynamic(() => import("../../components/Accounts"), {
|
const Accounts = dynamic(() => import('../../components/Accounts'), {
|
||||||
ssr: false,
|
ssr: false
|
||||||
});
|
})
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
// This and useEffect is here to prevent useLayoutEffect warnings from react-split
|
// This and useEffect is here to prevent useLayoutEffect warnings from react-split
|
||||||
const [showComponent, setShowComponent] = useState(false);
|
const [showComponent, setShowComponent] = useState(false)
|
||||||
const { transactionLogs } = useSnapshot(state);
|
const { transactionLogs } = useSnapshot(state)
|
||||||
const { transactions, activeHeader } = useSnapshot(transactionsState);
|
const { transactions, activeHeader } = useSnapshot(transactionsState)
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowComponent(true);
|
setShowComponent(true)
|
||||||
}, []);
|
}, [])
|
||||||
if (!showComponent) {
|
if (!showComponent) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
const hasScripts =
|
const hasScripts = Boolean(snap.files.filter(f => f.name.toLowerCase()?.endsWith('.js')).length)
|
||||||
snap.files.filter((f) => f.name.endsWith(".js")).length > 0;
|
|
||||||
|
const renderNav = () => (
|
||||||
|
<Flex css={{ gap: '$3' }}>
|
||||||
|
{snap.files
|
||||||
|
.filter(f => f.name.endsWith('.js'))
|
||||||
|
.map(file => (
|
||||||
|
<RunScript file={file} key={file.name} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container css={{ px: 0 }}>
|
<Container css={{ px: 0 }}>
|
||||||
<Split
|
<Split
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
sizes={
|
sizes={
|
||||||
getSplit("testVertical") || (hasScripts ? [50, 20, 30] : [50, 50])
|
hasScripts && getSplit('testVertical')?.length === 2
|
||||||
|
? [50, 20, 30]
|
||||||
|
: hasScripts
|
||||||
|
? [50, 20, 50]
|
||||||
|
: [50, 50]
|
||||||
}
|
}
|
||||||
gutterSize={4}
|
gutterSize={4}
|
||||||
gutterAlign="center"
|
gutterAlign="center"
|
||||||
style={{ height: "calc(100vh - 60px)" }}
|
style={{ height: 'calc(100vh - 60px)' }}
|
||||||
onDragEnd={(e) => saveSplit("testVertical", e)}
|
onDragEnd={e => saveSplit('testVertical', e)}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
row
|
row
|
||||||
fluid
|
fluid
|
||||||
css={{
|
css={{
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
p: "$3 $2",
|
p: '$3 $2'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Split
|
<Split
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
sizes={getSplit("testHorizontal") || [50, 50]}
|
sizes={getSplit('testHorizontal') || [50, 50]}
|
||||||
minSize={[180, 320]}
|
minSize={[180, 320]}
|
||||||
gutterSize={4}
|
gutterSize={4}
|
||||||
gutterAlign="center"
|
gutterAlign="center"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "row",
|
flexDirection: 'row',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
height: "100%",
|
height: '100%'
|
||||||
}}
|
}}
|
||||||
onDragEnd={(e) => saveSplit("testHorizontal", e)}
|
onDragEnd={e => saveSplit('testHorizontal', e)}
|
||||||
>
|
>
|
||||||
<Box css={{ width: "55%", px: "$2" }}>
|
<Box css={{ width: '55%', px: '$2' }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
label="Transaction"
|
||||||
activeHeader={activeHeader}
|
activeHeader={activeHeader}
|
||||||
// TODO make header a required field
|
// TODO make header a required field
|
||||||
onChangeActive={(idx, header) => {
|
onChangeActive={(idx, header) => {
|
||||||
if (header) transactionsState.activeHeader = header;
|
if (header) transactionsState.activeHeader = header
|
||||||
}}
|
}}
|
||||||
keepAllAlive
|
keepAllAlive
|
||||||
forceDefaultExtension
|
defaultExtension="json"
|
||||||
defaultExtension=".json"
|
allowedExtensions={['json']}
|
||||||
onCreateNewTab={(header) => modifyTransaction(header, {})}
|
onCreateNewTab={header => modifyTxState(header, {})}
|
||||||
onCloseTab={(idx, header) =>
|
onRenameTab={(idx, nwName, oldName = '') => renameTxState(oldName, nwName)}
|
||||||
header && modifyTransaction(header, undefined)
|
onCloseTab={(idx, header) => header && modifyTxState(header, undefined)}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{transactions.map(({ header, state }) => (
|
{transactions.map(({ header, state }) => (
|
||||||
<Tab key={header} header={header}>
|
<Tab key={header} header={header}>
|
||||||
@@ -90,27 +105,29 @@ const Test = () => {
|
|||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
<Box css={{ width: "45%", mx: "$2", height: "100%" }}>
|
<Box css={{ width: '45%', mx: '$2', height: '100%' }}>
|
||||||
<Accounts card hideDeployBtn showHookStats />
|
<Accounts card hideDeployBtn showHookStats />
|
||||||
</Box>
|
</Box>
|
||||||
</Split>
|
</Split>
|
||||||
</Flex>
|
</Flex>
|
||||||
{hasScripts && (
|
{hasScripts ? (
|
||||||
<Flex
|
<Flex
|
||||||
as="div"
|
as="div"
|
||||||
css={{
|
css={{
|
||||||
borderTop: "1px solid $mauve6",
|
borderTop: '1px solid $mauve6',
|
||||||
background: "$mauve1",
|
background: '$mauve1',
|
||||||
flexDirection: "column",
|
flexDirection: 'column'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogBoxForScripts
|
<LogBox
|
||||||
|
Icon={FileJs}
|
||||||
title="Helper scripts"
|
title="Helper scripts"
|
||||||
logs={snap.scriptLogs}
|
logs={snap.scriptLogs}
|
||||||
clearLog={() => (state.scriptLogs = [])}
|
clearLog={() => (state.scriptLogs = [])}
|
||||||
|
renderNav={renderNav}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
) : null}
|
||||||
<Flex>
|
<Flex>
|
||||||
<Split
|
<Split
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
@@ -119,16 +136,16 @@ const Test = () => {
|
|||||||
gutterSize={4}
|
gutterSize={4}
|
||||||
gutterAlign="center"
|
gutterAlign="center"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "row",
|
flexDirection: 'row',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
height: "100%",
|
height: '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
borderRight: "1px solid $mauve8",
|
borderRight: '1px solid $mauve8',
|
||||||
height: "100%",
|
height: '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogBox
|
<LogBox
|
||||||
@@ -137,14 +154,14 @@ const Test = () => {
|
|||||||
clearLog={() => (state.transactionLogs = [])}
|
clearLog={() => (state.transactionLogs = [])}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box css={{ height: "100%" }}>
|
<Box css={{ height: '100%' }}>
|
||||||
<DebugStream />
|
<DebugStream />
|
||||||
</Box>
|
</Box>
|
||||||
</Split>
|
</Split>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Split>
|
</Split>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Test;
|
export default Test
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
3304
patches/ripple-binary-codec+1.4.2.patch
Normal file
3304
patches/ripple-binary-codec+1.4.2.patch
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "Hooks Builder",
|
"name": "Hooks Builder",
|
||||||
"short_name": "Hooks Builder",
|
"short_name": "Hooks Builder",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/android-chrome-192x192.png",
|
"src": "/android-chrome-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/android-chrome-512x512.png",
|
"src": "/android-chrome-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"theme_color": "#161618",
|
"theme_color": "#161618",
|
||||||
"background_color": "#161618",
|
"background_color": "#161618",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|||||||
13
raw-loader.d.ts
vendored
13
raw-loader.d.ts
vendored
@@ -1,4 +1,9 @@
|
|||||||
declare module "*.md" {
|
declare module '*.md' {
|
||||||
const content: string;
|
const content: string
|
||||||
export default content;
|
export default content
|
||||||
};
|
}
|
||||||
|
|
||||||
|
declare module '*.hook-bundle.js' {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
|
import toast from 'react-hot-toast'
|
||||||
import toast from "react-hot-toast";
|
import state, { FaucetAccountRes } from '../index'
|
||||||
import state, { FaucetAccountRes } from '../index';
|
import fetchAccountInfo from '../../utils/accountInfo';
|
||||||
|
|
||||||
export const names = [
|
export const names = [
|
||||||
"Alice",
|
'Alice',
|
||||||
"Bob",
|
'Bob',
|
||||||
"Carol",
|
'Carol',
|
||||||
"Carlos",
|
'Carlos',
|
||||||
"Charlie",
|
'Charlie',
|
||||||
"Dan",
|
'Dan',
|
||||||
"Dave",
|
'Dave',
|
||||||
"David",
|
'David',
|
||||||
"Faythe",
|
'Faythe',
|
||||||
"Frank",
|
'Frank',
|
||||||
"Grace",
|
'Grace',
|
||||||
"Heidi",
|
'Heidi',
|
||||||
"Judy",
|
'Judy',
|
||||||
"Olive",
|
'Olive',
|
||||||
"Peggy",
|
'Peggy',
|
||||||
"Walter",
|
'Walter'
|
||||||
];
|
]
|
||||||
|
|
||||||
/* This function adds faucet account to application global state.
|
/* This function adds faucet account to application global state.
|
||||||
* It calls the /api/faucet endpoint which in send a HTTP POST to
|
* It calls the /api/faucet endpoint which in send a HTTP POST to
|
||||||
@@ -27,70 +27,60 @@ export const names = [
|
|||||||
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
|
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
|
||||||
* is protected with CORS so that's why we did our own endpoint
|
* is protected with CORS so that's why we did our own endpoint
|
||||||
*/
|
*/
|
||||||
export const addFaucetAccount = async (showToast: boolean = false) => {
|
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
|
||||||
// Lets limit the number of faucet accounts to 5 for now
|
if (typeof window === undefined) return
|
||||||
if (state.accounts.length > 5) {
|
|
||||||
return toast.error("You can only have maximum 6 accounts");
|
const toastId = showToast ? toast.loading('Creating account') : ''
|
||||||
|
const res = await fetch(`${window.location.origin}/api/faucet`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
const json: FaucetAccountRes | { error: string } = await res.json()
|
||||||
|
if ('error' in json) {
|
||||||
|
if (!showToast) return;
|
||||||
|
return toast.error(json.error, { id: toastId })
|
||||||
}
|
}
|
||||||
if (typeof window !== 'undefined') {
|
const currNames = state.accounts.map(acc => acc.name)
|
||||||
|
const info = await fetchAccountInfo(json.address, { silent: true })
|
||||||
|
state.accounts.push({
|
||||||
|
name: name || names.filter(name => !currNames.includes(name))[0],
|
||||||
|
xrp: (json.xrp || 0 * 1000000).toString(),
|
||||||
|
address: json.address,
|
||||||
|
secret: json.secret,
|
||||||
|
sequence: info?.Sequence || 1,
|
||||||
|
hooks: [],
|
||||||
|
isLoading: false,
|
||||||
|
version: '2'
|
||||||
|
})
|
||||||
|
if (showToast) {
|
||||||
|
toast.success('New account created', { id: toastId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch initial faucets
|
||||||
const toastId = showToast ? toast.loading("Creating account") : "";
|
; (async function fetchFaucets() {
|
||||||
const res = await fetch(`${window.location.origin}/api/faucet`, {
|
if (typeof window !== 'undefined') {
|
||||||
method: "POST",
|
if (state.accounts.length === 0) {
|
||||||
});
|
await addFaucetAccount()
|
||||||
const json: FaucetAccountRes | { error: string } = await res.json();
|
// setTimeout(() => {
|
||||||
if ("error" in json) {
|
// addFaucetAccount();
|
||||||
if (showToast) {
|
// }, 10000);
|
||||||
return toast.error(json.error, { id: toastId });
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (showToast) {
|
|
||||||
toast.success("New account created", { id: toastId });
|
|
||||||
}
|
|
||||||
const currNames = state.accounts.map(acc => acc.name);
|
|
||||||
state.accounts.push({
|
|
||||||
name: names.filter(name => !currNames.includes(name))[0],
|
|
||||||
xrp: (json.xrp || 0 * 1000000).toString(),
|
|
||||||
address: json.address,
|
|
||||||
secret: json.secret,
|
|
||||||
sequence: 1,
|
|
||||||
hooks: [],
|
|
||||||
isLoading: false,
|
|
||||||
version: '2'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
})()
|
||||||
};
|
|
||||||
|
|
||||||
// fetch initial faucets
|
|
||||||
(async function fetchFaucets() {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
if (state.accounts.length === 0) {
|
|
||||||
await addFaucetAccount();
|
|
||||||
// setTimeout(() => {
|
|
||||||
// addFaucetAccount();
|
|
||||||
// }, 10000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
export const addFunds = async (address: string) => {
|
export const addFunds = async (address: string) => {
|
||||||
const toastId = toast.loading("Requesting funds");
|
const toastId = toast.loading('Requesting funds')
|
||||||
const res = await fetch(`${window.location.origin}/api/faucet?account=${address}`, {
|
const res = await fetch(`${window.location.origin}/api/faucet?account=${address}`, {
|
||||||
method: "POST",
|
method: 'POST'
|
||||||
});
|
})
|
||||||
const json: FaucetAccountRes | { error: string } = await res.json();
|
const json: FaucetAccountRes | { error: string } = await res.json()
|
||||||
if ("error" in json) {
|
if ('error' in json) {
|
||||||
return toast.error(json.error, { id: toastId });
|
return toast.error(json.error, { id: toastId })
|
||||||
} else {
|
} else {
|
||||||
toast.success(`Funds added (${json.xrp} XRP)`, { id: toastId });
|
toast.success(`Funds added (${json.xrp} XRP)`, { id: toastId })
|
||||||
const currAccount = state.accounts.find(acc => acc.address === address);
|
const currAccount = state.accounts.find(acc => acc.address === address)
|
||||||
if (currAccount) {
|
if (currAccount) {
|
||||||
currAccount.xrp = (Number(currAccount.xrp) + (json.xrp * 1000000)).toString();
|
currAccount.xrp = (Number(currAccount.xrp) + json.xrp * 1000000).toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,93 +1,171 @@
|
|||||||
import toast from "react-hot-toast";
|
import toast from 'react-hot-toast'
|
||||||
import Router from 'next/router';
|
import Router from 'next/router'
|
||||||
|
|
||||||
import state from "../index";
|
import state from '../index'
|
||||||
import { saveFile } from "./saveFile";
|
import { saveFile } from './saveFile'
|
||||||
import { decodeBinary } from "../../utils/decodeBinary";
|
import { decodeBinary } from '../../utils/decodeBinary'
|
||||||
import { ref } from "valtio";
|
import { ref } from 'valtio'
|
||||||
|
|
||||||
/* compileCode sends the code of the active file to compile endpoint
|
/* compileCode sends the code of the active file to compile endpoint
|
||||||
* If all goes well you will get base64 encoded wasm file back with
|
* If all goes well you will get base64 encoded wasm file back with
|
||||||
* some extra logging information if we can provide it. This function
|
* some extra logging information if we can provide it. This function
|
||||||
* also decodes the returned wasm and creates human readable WAT file
|
* also decodes the returned wasm and creates human readable WAT file
|
||||||
* out of it and store both in global state.
|
* out of it and store both in global state.
|
||||||
*/
|
*/
|
||||||
export const compileCode = async (activeId: number) => {
|
export const compileCode = async (activeId: number) => {
|
||||||
// Save the file to global state
|
// Save the file to global state
|
||||||
saveFile(false);
|
saveFile(false, activeId)
|
||||||
|
const file = state.files[activeId]
|
||||||
|
if (file.name.endsWith('.wat')) {
|
||||||
|
return compileWat(activeId)
|
||||||
|
}
|
||||||
|
|
||||||
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
|
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
|
||||||
throw Error("Missing env!");
|
throw Error('Missing env!')
|
||||||
}
|
}
|
||||||
// Bail out if we're already compiling
|
// Bail out if we're already compiling
|
||||||
if (state.compiling) {
|
if (state.compiling) {
|
||||||
// if compiling is ongoing return
|
// if compiling is ongoing return // TODO Inform user about it.
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
// Set loading state to true
|
// Set loading state to true
|
||||||
state.compiling = true;
|
state.compiling = true
|
||||||
state.logs = []
|
state.logs = []
|
||||||
try {
|
try {
|
||||||
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
|
file.containsErrors = false
|
||||||
method: "POST",
|
let res: Response
|
||||||
headers: {
|
try {
|
||||||
"Content-Type": "application/json",
|
res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
|
||||||
},
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
headers: {
|
||||||
output: "wasm",
|
'Content-Type': 'application/json'
|
||||||
compress: true,
|
},
|
||||||
strip: state.compileOptions.strip,
|
body: JSON.stringify({
|
||||||
files: [
|
output: 'wasm',
|
||||||
{
|
compress: true,
|
||||||
type: "c",
|
strip: state.compileOptions.strip,
|
||||||
options: state.compileOptions.optimizationLevel || '-O0',
|
files: [
|
||||||
name: state.files[activeId].name,
|
{
|
||||||
src: state.files[activeId].content,
|
type: 'c',
|
||||||
},
|
options: state.compileOptions.optimizationLevel || '-O2',
|
||||||
],
|
name: file.name,
|
||||||
}),
|
src: file.content
|
||||||
});
|
}
|
||||||
const json = await res.json();
|
]
|
||||||
state.compiling = false;
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
throw Error('Something went wrong, check your network connection and try again!')
|
||||||
|
}
|
||||||
|
const json = await res.json()
|
||||||
|
state.compiling = false
|
||||||
if (!json.success) {
|
if (!json.success) {
|
||||||
state.logs.push({ type: "error", message: json.message });
|
const errors = [json.message]
|
||||||
if (json.tasks && json.tasks.length > 0) {
|
if (json.tasks && json.tasks.length > 0) {
|
||||||
json.tasks.forEach((task: any) => {
|
json.tasks.forEach((task: any) => {
|
||||||
if (!task.success) {
|
if (!task.success) {
|
||||||
state.logs.push({ type: "error", message: task?.console });
|
errors.push(task?.console)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
return toast.error(`Couldn't compile!`, { position: "bottom-center" });
|
throw errors
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Decode base64 encoded wasm that is coming back from the endpoint
|
||||||
|
const bufferData = await decodeBinary(json.output)
|
||||||
|
|
||||||
|
// Import wabt from and create human readable version of wasm file and
|
||||||
|
// put it into state
|
||||||
|
const ww = await (await import('wabt')).default()
|
||||||
|
const myModule = ww.readWasm(new Uint8Array(bufferData), {
|
||||||
|
readDebugNames: true
|
||||||
|
})
|
||||||
|
myModule.applyNames()
|
||||||
|
|
||||||
|
const wast = myModule.toText({ foldExprs: false, inlineExport: false })
|
||||||
|
|
||||||
|
file.compiledContent = ref(bufferData)
|
||||||
|
file.lastCompiled = new Date()
|
||||||
|
file.compiledValueSnapshot = file.content
|
||||||
|
file.compiledWatContent = wast
|
||||||
|
} catch (error) {
|
||||||
|
throw Error('Invalid compilation result produced, check your code for errors and try again!')
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Compiled successfully!', { position: 'bottom-center' })
|
||||||
|
state.logs.push({
|
||||||
|
type: 'success',
|
||||||
|
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
|
||||||
|
link: Router.asPath.replace('develop', 'deploy'),
|
||||||
|
linkText: 'Go to deploy'
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
|
||||||
|
if (err instanceof Array && typeof err[0] === 'string') {
|
||||||
|
err.forEach(message => {
|
||||||
|
state.logs.push({
|
||||||
|
type: 'error',
|
||||||
|
message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if (err instanceof Error) {
|
||||||
|
state.logs.push({
|
||||||
|
type: 'error',
|
||||||
|
message: err.message
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
state.logs.push({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Something went wrong, come back later!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
state.compiling = false
|
||||||
|
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
|
||||||
|
file.containsErrors = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const compileWat = async (activeId: number) => {
|
||||||
|
if (state.compiling) return;
|
||||||
|
const file = state.files[activeId]
|
||||||
|
state.compiling = true
|
||||||
|
state.logs = []
|
||||||
|
try {
|
||||||
|
const wabt = await (await import('wabt')).default()
|
||||||
|
const module = wabt.parseWat(file.name, file.content);
|
||||||
|
module.resolveNames();
|
||||||
|
module.validate();
|
||||||
|
const { buffer } = module.toBinary({
|
||||||
|
log: false,
|
||||||
|
write_debug_names: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
file.compiledContent = ref(buffer)
|
||||||
|
file.lastCompiled = new Date()
|
||||||
|
file.compiledValueSnapshot = file.content
|
||||||
|
file.compiledWatContent = file.content
|
||||||
|
|
||||||
|
toast.success('Compiled successfully!', { position: 'bottom-center' })
|
||||||
|
state.logs.push({
|
||||||
|
type: 'success',
|
||||||
|
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
|
||||||
|
link: Router.asPath.replace('develop', 'deploy'),
|
||||||
|
linkText: 'Go to deploy'
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
let message = "Error compiling WAT file!"
|
||||||
|
if (err instanceof Error) {
|
||||||
|
message = err.message
|
||||||
}
|
}
|
||||||
state.logs.push({
|
state.logs.push({
|
||||||
type: "success",
|
type: 'error',
|
||||||
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
|
message
|
||||||
link: Router.asPath.replace("develop", "deploy"),
|
})
|
||||||
linkText: "Go to deploy",
|
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
|
||||||
});
|
file.containsErrors = true
|
||||||
// Decode base64 encoded wasm that is coming back from the endpoint
|
|
||||||
const bufferData = await decodeBinary(json.output);
|
|
||||||
state.files[state.active].compiledContent = ref(bufferData);
|
|
||||||
state.files[state.active].lastCompiled = new Date();
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
};
|
state.compiling = false
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
import state, { IFile } from '../index';
|
import { getFileExtention } from '../../utils/helpers'
|
||||||
|
import state, { IFile } from '../index'
|
||||||
|
|
||||||
|
const languageMapping: Record<string, string | undefined> = {
|
||||||
|
ts: 'typescript',
|
||||||
|
js: 'javascript',
|
||||||
|
md: 'markdown',
|
||||||
|
c: 'c',
|
||||||
|
h: 'c',
|
||||||
|
txt: 'text'
|
||||||
|
}
|
||||||
|
|
||||||
const languageMapping = {
|
|
||||||
'ts': 'typescript',
|
|
||||||
'js': 'javascript',
|
|
||||||
'md': 'markdown',
|
|
||||||
'c': 'c',
|
|
||||||
'h': 'c',
|
|
||||||
'other': ''
|
|
||||||
} /* Initializes empty file to global state */
|
|
||||||
export const createNewFile = (name: string) => {
|
export const createNewFile = (name: string) => {
|
||||||
const tempName = name.split('.');
|
const ext = getFileExtention(name) || ''
|
||||||
const fileExt = tempName[tempName.length - 1] || 'other';
|
|
||||||
const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
|
const emptyFile: IFile = { name, language: languageMapping[ext] || 'text', content: '' }
|
||||||
state.files.push(emptyFile);
|
state.files.push(emptyFile)
|
||||||
state.active = state.files.length - 1;
|
state.active = state.files.length - 1
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export const renameFile = (oldName: string, nwName: string) => {
|
||||||
|
const file = state.files.find(file => file.name === oldName)
|
||||||
|
if (!file) throw Error(`No file exists with name ${oldName}`)
|
||||||
|
|
||||||
|
const ext = getFileExtention(nwName) || ''
|
||||||
|
const language = languageMapping[ext] || 'text'
|
||||||
|
file.name = nwName
|
||||||
|
file.language = language
|
||||||
|
}
|
||||||
|
|||||||
24
state/actions/deleteAccount.ts
Normal file
24
state/actions/deleteAccount.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import state, { transactionsState } from '..'
|
||||||
|
|
||||||
|
export const deleteAccount = (addr?: string) => {
|
||||||
|
if (!addr) return
|
||||||
|
const index = state.accounts.findIndex(acc => acc.address === addr)
|
||||||
|
if (index === -1) return
|
||||||
|
state.accounts.splice(index, 1)
|
||||||
|
|
||||||
|
// update selected accounts
|
||||||
|
transactionsState.transactions
|
||||||
|
.filter(t => t.state.selectedAccount?.value === addr)
|
||||||
|
.forEach(t => {
|
||||||
|
const acc = t.state.selectedAccount
|
||||||
|
if (!acc) return
|
||||||
|
acc.label = acc.value
|
||||||
|
})
|
||||||
|
transactionsState.transactions
|
||||||
|
.filter(t => t.state.selectedDestAccount?.value === addr)
|
||||||
|
.forEach(t => {
|
||||||
|
const acc = t.state.selectedDestAccount
|
||||||
|
if (!acc) return
|
||||||
|
acc.label = acc.value
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,85 +1,74 @@
|
|||||||
import { derive, sign } from "xrpl-accountlib";
|
import { derive, sign } from 'xrpl-accountlib'
|
||||||
import toast from "react-hot-toast";
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import state, { IAccount } from "../index";
|
import state, { IAccount } from '../index'
|
||||||
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
|
import calculateHookOn, { TTS } from '../../utils/hookOnCalculator'
|
||||||
import { SetHookData } from "../../components/SetHookDialog";
|
import { Link } from '../../components'
|
||||||
import { Link } from "../../components";
|
import { ref } from 'valtio'
|
||||||
import { ref } from "valtio";
|
import estimateFee from '../../utils/estimateFee'
|
||||||
import estimateFee from "../../utils/estimateFee";
|
import { SetHookData, toHex } from '../../utils/setHook'
|
||||||
|
import ResultLink from '../../components/ResultLink'
|
||||||
|
import { xrplSend } from './xrpl-client'
|
||||||
|
|
||||||
export const sha256 = async (string: string) => {
|
export const sha256 = async (string: string) => {
|
||||||
const utf8 = new TextEncoder().encode(string);
|
const utf8 = new TextEncoder().encode(string)
|
||||||
const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
|
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8)
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
const hashHex = hashArray
|
const hashHex = hashArray.map(bytes => bytes.toString(16).padStart(2, '0')).join('')
|
||||||
.map((bytes) => bytes.toString(16).padStart(2, "0"))
|
return hashHex
|
||||||
.join("");
|
|
||||||
return hashHex;
|
|
||||||
};
|
|
||||||
|
|
||||||
function toHex(str: string) {
|
|
||||||
var result = "";
|
|
||||||
for (var i = 0; i < str.length; i++) {
|
|
||||||
result += str.charCodeAt(i).toString(16);
|
|
||||||
}
|
|
||||||
return result.toUpperCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
|
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
|
||||||
if (!arrayBuffer) {
|
if (!arrayBuffer) {
|
||||||
return "";
|
return ''
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
typeof arrayBuffer !== "object" ||
|
typeof arrayBuffer !== 'object' ||
|
||||||
arrayBuffer === null ||
|
arrayBuffer === null ||
|
||||||
typeof arrayBuffer.byteLength !== "number"
|
typeof arrayBuffer.byteLength !== 'number'
|
||||||
) {
|
) {
|
||||||
throw new TypeError("Expected input to be an ArrayBuffer");
|
throw new TypeError('Expected input to be an ArrayBuffer')
|
||||||
}
|
}
|
||||||
|
|
||||||
var view = new Uint8Array(arrayBuffer);
|
var view = new Uint8Array(arrayBuffer)
|
||||||
var result = "";
|
var result = ''
|
||||||
var value;
|
var value
|
||||||
|
|
||||||
for (var i = 0; i < view.length; i++) {
|
for (var i = 0; i < view.length; i++) {
|
||||||
value = view[i].toString(16);
|
value = view[i].toString(16)
|
||||||
result += value.length === 1 ? "0" + value : value;
|
result += value.length === 1 ? '0' + value : value
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const prepareDeployHookTx = async (
|
export const prepareDeployHookTx = async (
|
||||||
account: IAccount & { name?: string },
|
account: IAccount & { name?: string },
|
||||||
data: SetHookData
|
data: SetHookData
|
||||||
) => {
|
) => {
|
||||||
if (
|
const activeFile = state.files[state.active]?.compiledContent
|
||||||
!state.files ||
|
? state.files[state.active]
|
||||||
state.files.length === 0 ||
|
: state.files.filter(file => file.compiledContent)[0]
|
||||||
!state.files?.[state.active]?.compiledContent
|
|
||||||
) {
|
if (!state.files || state.files.length === 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.files?.[state.active]?.compiledContent) {
|
if (!activeFile?.compiledContent) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (!state.client) {
|
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase()
|
||||||
return;
|
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value)
|
||||||
}
|
const { HookParameters } = data
|
||||||
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase();
|
|
||||||
const hookOnValues: (keyof TTS)[] = data.Invoke.map((tt) => tt.value);
|
|
||||||
const { HookParameters } = data;
|
|
||||||
const filteredHookParameters = HookParameters.filter(
|
const filteredHookParameters = HookParameters.filter(
|
||||||
(hp) =>
|
hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
|
||||||
hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
|
)?.map(aa => ({
|
||||||
)?.map((aa) => ({
|
|
||||||
HookParameter: {
|
HookParameter: {
|
||||||
HookParameterName: toHex(aa.HookParameter.HookParameterName || ""),
|
HookParameterName: toHex(aa.HookParameter.HookParameterName || ''),
|
||||||
HookParameterValue: aa.HookParameter.HookParameterValue || "",
|
HookParameterValue: aa.HookParameter.HookParameterValue || ''
|
||||||
},
|
}
|
||||||
}));
|
}))
|
||||||
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
|
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
|
||||||
// return {
|
// return {
|
||||||
// HookGrant: {
|
// HookGrant: {
|
||||||
@@ -89,194 +78,184 @@ export const prepareDeployHookTx = async (
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window === 'undefined') return
|
||||||
const tx = {
|
const tx = {
|
||||||
Account: account.address,
|
Account: account.address,
|
||||||
TransactionType: "SetHook",
|
TransactionType: 'SetHook',
|
||||||
Sequence: account.sequence,
|
Sequence: account.sequence,
|
||||||
Fee: data.Fee,
|
Fee: data.Fee,
|
||||||
Hooks: [
|
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
|
||||||
{
|
Hooks: [
|
||||||
Hook: {
|
{
|
||||||
CreateCode: arrayBufferToHex(
|
Hook: {
|
||||||
state.files?.[state.active]?.compiledContent
|
CreateCode: arrayBufferToHex(activeFile?.compiledContent).toUpperCase(),
|
||||||
).toUpperCase(),
|
HookOn: calculateHookOn(hookOnValues),
|
||||||
HookOn: calculateHookOn(hookOnValues),
|
HookNamespace,
|
||||||
HookNamespace,
|
HookApiVersion: 0,
|
||||||
HookApiVersion: 0,
|
Flags: 1,
|
||||||
Flags: 1,
|
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
|
||||||
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
|
...(filteredHookParameters.length > 0 && {
|
||||||
...(filteredHookParameters.length > 0 && {
|
HookParameters: filteredHookParameters
|
||||||
HookParameters: filteredHookParameters,
|
})
|
||||||
}),
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return tx;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* 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 },
|
|
||||||
data: SetHookData
|
|
||||||
) => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const tx = await prepareDeployHookTx(account, data);
|
|
||||||
if (!tx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state.client) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
let submitRes;
|
|
||||||
|
|
||||||
try {
|
|
||||||
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: ref(
|
|
||||||
<>
|
|
||||||
[{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
|
|
||||||
Transaction hash:{" "}
|
|
||||||
<Link
|
|
||||||
as="a"
|
|
||||||
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.tx_json?.hash}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{submitRes.tx_json?.hash}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
// 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.error}] ${
|
|
||||||
submitRes.engine_result_message || submitRes.error_exception
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
]
|
||||||
console.log(err);
|
|
||||||
state.deployLogs.push({
|
|
||||||
type: "error",
|
|
||||||
message: "Error occured while deploying",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (currentAccount) {
|
|
||||||
currentAccount.isLoading = false;
|
|
||||||
}
|
|
||||||
return submitRes;
|
|
||||||
}
|
}
|
||||||
};
|
return tx
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Turns the wasm binary into hex string, signs the transaction and deploys it to Hooks testnet.
|
||||||
|
*/
|
||||||
|
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
|
||||||
|
const activeFile = state.files[state.active]?.compiledContent
|
||||||
|
? state.files[state.active]
|
||||||
|
: state.files.filter(file => file.compiledContent)[0]
|
||||||
|
state.deployValues[activeFile.name] = data
|
||||||
|
|
||||||
|
const tx = await prepareDeployHookTx(account, data)
|
||||||
|
if (!tx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
let submitRes
|
||||||
|
try {
|
||||||
|
submitRes = await xrplSend({
|
||||||
|
command: 'submit',
|
||||||
|
tx_blob: signedTransaction
|
||||||
|
})
|
||||||
|
|
||||||
|
const txHash = submitRes.tx_json?.hash
|
||||||
|
const resultMsg = ref(
|
||||||
|
<>
|
||||||
|
[<ResultLink result={submitRes.engine_result} />] {submitRes.engine_result_message}{' '}
|
||||||
|
{txHash && (
|
||||||
|
<>
|
||||||
|
Transaction hash:{' '}
|
||||||
|
<Link
|
||||||
|
as="a"
|
||||||
|
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${txHash}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{txHash}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
if (submitRes.engine_result === 'tesSUCCESS') {
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Hook deployed successfully ✅'
|
||||||
|
})
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: 'success',
|
||||||
|
message: resultMsg
|
||||||
|
})
|
||||||
|
} else if (submitRes.engine_result) {
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: 'error',
|
||||||
|
message: resultMsg
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: 'error',
|
||||||
|
message: `[${submitRes.error}] ${submitRes.error_exception}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Error occurred while deploying'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (currentAccount) {
|
||||||
|
currentAccount.isLoading = false
|
||||||
|
}
|
||||||
|
return submitRes
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteHook = async (account: IAccount & { name?: string }) => {
|
export const deleteHook = async (account: IAccount & { name?: string }) => {
|
||||||
if (!state.client) {
|
const currentAccount = state.accounts.find(acc => acc.address === account.address)
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentAccount = state.accounts.find(
|
|
||||||
(acc) => acc.address === account.address
|
|
||||||
);
|
|
||||||
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
|
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (typeof window !== "undefined") {
|
const tx = {
|
||||||
const tx = {
|
Account: account.address,
|
||||||
Account: account.address,
|
TransactionType: 'SetHook',
|
||||||
TransactionType: "SetHook",
|
Sequence: account.sequence,
|
||||||
Sequence: account.sequence,
|
Fee: '100000',
|
||||||
Fee: "100000",
|
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
|
||||||
Hooks: [
|
Hooks: [
|
||||||
{
|
{
|
||||||
Hook: {
|
Hook: {
|
||||||
CreateCode: "",
|
CreateCode: '',
|
||||||
Flags: 1,
|
Flags: 1
|
||||||
},
|
}
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const keypair = derive.familySeed(account.secret);
|
|
||||||
try {
|
|
||||||
// Update tx Fee value with network estimation
|
|
||||||
const res = await estimateFee(tx, account);
|
|
||||||
tx["Fee"] = res?.base_fee ? res?.base_fee : "1000";
|
|
||||||
} catch (err) {
|
|
||||||
// use default value what you defined earlier
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
const { signedTransaction } = sign(tx, keypair);
|
|
||||||
|
|
||||||
if (currentAccount) {
|
|
||||||
currentAccount.isLoading = true;
|
|
||||||
}
|
|
||||||
let submitRes;
|
|
||||||
const toastId = toast.loading("Deleting hook...");
|
|
||||||
try {
|
|
||||||
submitRes = await state.client.send({
|
|
||||||
command: "submit",
|
|
||||||
tx_blob: signedTransaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (submitRes.engine_result === "tesSUCCESS") {
|
|
||||||
toast.success("Hook deleted successfully ✅", { id: toastId });
|
|
||||||
state.deployLogs.push({
|
|
||||||
type: "success",
|
|
||||||
message: "Hook deleted successfully ✅",
|
|
||||||
});
|
|
||||||
state.deployLogs.push({
|
|
||||||
type: "success",
|
|
||||||
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
|
|
||||||
});
|
|
||||||
currentAccount.hooks = [];
|
|
||||||
} else {
|
|
||||||
toast.error(
|
|
||||||
`${submitRes.engine_result_message || submitRes.error_exception}`,
|
|
||||||
{ id: toastId }
|
|
||||||
);
|
|
||||||
state.deployLogs.push({
|
|
||||||
type: "error",
|
|
||||||
message: `[${submitRes.engine_result || submitRes.error}] ${
|
|
||||||
submitRes.engine_result_message || submitRes.error_exception
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
]
|
||||||
console.log(err);
|
|
||||||
toast.error("Error occured while deleting hoook", { id: toastId });
|
|
||||||
state.deployLogs.push({
|
|
||||||
type: "error",
|
|
||||||
message: "Error occured while deleting hook",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (currentAccount) {
|
|
||||||
currentAccount.isLoading = false;
|
|
||||||
}
|
|
||||||
return submitRes;
|
|
||||||
}
|
}
|
||||||
};
|
const keypair = derive.familySeed(account.secret)
|
||||||
|
try {
|
||||||
|
// Update tx Fee value with network estimation
|
||||||
|
const res = await estimateFee(tx, account)
|
||||||
|
tx['Fee'] = res?.base_fee || '1000'
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
const { signedTransaction } = sign(tx, keypair)
|
||||||
|
if (currentAccount) {
|
||||||
|
currentAccount.isLoading = true
|
||||||
|
}
|
||||||
|
let submitRes
|
||||||
|
const toastId = toast.loading('Deleting hook...')
|
||||||
|
try {
|
||||||
|
submitRes = await xrplSend({
|
||||||
|
command: 'submit',
|
||||||
|
tx_blob: signedTransaction
|
||||||
|
})
|
||||||
|
|
||||||
|
if (submitRes.engine_result === 'tesSUCCESS') {
|
||||||
|
toast.success('Hook deleted successfully ✅', { id: toastId })
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Hook deleted successfully ✅'
|
||||||
|
})
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: 'success',
|
||||||
|
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`
|
||||||
|
})
|
||||||
|
currentAccount.hooks = []
|
||||||
|
} else {
|
||||||
|
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, {
|
||||||
|
id: toastId
|
||||||
|
})
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: 'error',
|
||||||
|
message: `[${submitRes.engine_result || submitRes.error}] ${
|
||||||
|
submitRes.engine_result_message || submitRes.error_exception
|
||||||
|
}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
toast.error('Error occurred while deleting hook', { id: toastId })
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Error occurred while deleting hook'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (currentAccount) {
|
||||||
|
currentAccount.isLoading = false
|
||||||
|
}
|
||||||
|
return submitRes
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { createZip } from '../../utils/zip';
|
import { createZip } from '../../utils/zip'
|
||||||
import { guessZipFileName } from '../../utils/helpers';
|
import { guessZipFileName } from '../../utils/helpers'
|
||||||
import state from '..'
|
import state from '..'
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
export const downloadAsZip = async () => {
|
export const downloadAsZip = async () => {
|
||||||
try {
|
try {
|
||||||
state.zipLoading = true
|
state.zipLoading = true
|
||||||
// TODO do something about file/gist loading state
|
// TODO do something about file/gist loading state
|
||||||
const files = state.files.map(({ name, content }) => ({ name, content }));
|
const files = state.files.map(({ name, content }) => ({ name, content }))
|
||||||
const wasmFiles = state.files.filter(i => i.compiledContent).map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent }));
|
const wasmFiles = state.files
|
||||||
const zipped = await createZip([...files, ...wasmFiles]);
|
.filter(i => i.compiledContent)
|
||||||
const zipFileName = guessZipFileName(files);
|
.map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent }))
|
||||||
zipped.saveFile(zipFileName);
|
const zipped = await createZip([...files, ...wasmFiles])
|
||||||
} catch (error) {
|
const zipFileName = guessZipFileName(files)
|
||||||
toast.error('Error occured while creating zip file, try again later')
|
zipped.saveFile(zipFileName)
|
||||||
} finally {
|
} catch (error) {
|
||||||
state.zipLoading = false
|
toast.error('Error occurred while creating zip file, try again later')
|
||||||
}
|
} finally {
|
||||||
};
|
state.zipLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,117 +1,100 @@
|
|||||||
import { Octokit } from "@octokit/core";
|
import { Octokit } from '@octokit/core'
|
||||||
import Router from "next/router";
|
import state, { IFile } from '../index'
|
||||||
import state from '../index';
|
import { templateFileIds } from '../constants'
|
||||||
import { templateFileIds } from '../constants';
|
|
||||||
|
|
||||||
const octokit = new Octokit();
|
const octokit = new Octokit()
|
||||||
|
|
||||||
/* Fetches Gist files from Githug Gists based on
|
/**
|
||||||
* gistId and stores the content in global state
|
* Fetches files from Github Gists based on gistId and stores them in global state
|
||||||
*/
|
*/
|
||||||
export const fetchFiles = (gistId: string) => {
|
export const fetchFiles = async (gistId: string) => {
|
||||||
state.loading = true;
|
if (!gistId || state.files.length) return
|
||||||
if (gistId && !state.files.length) {
|
|
||||||
|
state.loading = true
|
||||||
|
state.logs.push({
|
||||||
|
type: 'log',
|
||||||
|
message: `Fetching Gist with id: ${gistId}`
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const res = await octokit.request('GET /gists/{gist_id}', { gist_id: gistId })
|
||||||
|
|
||||||
|
const isTemplate = (id: string) =>
|
||||||
|
Object.values(templateFileIds)
|
||||||
|
.map(v => v.id)
|
||||||
|
.includes(id)
|
||||||
|
|
||||||
|
if (isTemplate(gistId)) {
|
||||||
|
// fetch headers
|
||||||
|
const headerRes = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`
|
||||||
|
)
|
||||||
|
if (!headerRes.ok) throw Error('Failed to fetch headers')
|
||||||
|
|
||||||
|
const headerJson = await headerRes.json()
|
||||||
|
const headerFiles: Record<string, { filename: string; content: string; language: string }> =
|
||||||
|
{}
|
||||||
|
Object.entries(headerJson).forEach(([key, value]) => {
|
||||||
|
const fname = `${key}.h`
|
||||||
|
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' }
|
||||||
|
})
|
||||||
|
const files = {
|
||||||
|
...res.data.files,
|
||||||
|
...headerFiles
|
||||||
|
}
|
||||||
|
res.data.files = files
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.data.files) throw Error('No files could be fetched from given gist id!')
|
||||||
|
|
||||||
|
const files: IFile[] = Object.keys(res.data.files).map(filename => ({
|
||||||
|
name: res.data.files?.[filename]?.filename || 'untitled.c',
|
||||||
|
language: res.data.files?.[filename]?.language?.toLowerCase() || '',
|
||||||
|
content: res.data.files?.[filename]?.content || ''
|
||||||
|
}))
|
||||||
|
|
||||||
|
files.sort((a, b) => {
|
||||||
|
const aBasename = a.name.split('.')?.[0]
|
||||||
|
const aExt = a.name.split('.').pop() || ''
|
||||||
|
const bBasename = b.name.split('.')?.[0]
|
||||||
|
const bExt = b.name.split('.').pop() || ''
|
||||||
|
|
||||||
|
// default priority is undefined == 0
|
||||||
|
const extPriority: Record<string, number> = {
|
||||||
|
c: 3,
|
||||||
|
wat: 3,
|
||||||
|
md: 2,
|
||||||
|
h: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort based on extention priorities
|
||||||
|
const comp = (extPriority[bExt] || 0) - (extPriority[aExt] || 0)
|
||||||
|
if (comp !== 0) return comp
|
||||||
|
|
||||||
|
// Otherwise fallback to alphabetical sorting
|
||||||
|
return aBasename.localeCompare(bBasename)
|
||||||
|
})
|
||||||
|
|
||||||
state.logs.push({
|
state.logs.push({
|
||||||
type: "log",
|
type: 'success',
|
||||||
message: `Fetching Gist with id: ${gistId}`,
|
message: 'Fetched successfully ✅'
|
||||||
});
|
})
|
||||||
|
state.files = files
|
||||||
|
state.gistId = gistId
|
||||||
|
state.gistOwner = res.data.owner?.login
|
||||||
|
|
||||||
octokit
|
const gistName =
|
||||||
.request("GET /gists/{gist_id}", { gist_id: gistId })
|
files.find(file => file.language === 'c' || file.language === 'javascript')?.name ||
|
||||||
.then(async res => {
|
'untitled'
|
||||||
if (!Object.values(templateFileIds).includes(gistId)) {
|
state.gistName = gistName
|
||||||
return res
|
} catch (err) {
|
||||||
}
|
console.error(err)
|
||||||
// in case of templates, fetch header file(s) and append to res
|
let message: string
|
||||||
try {
|
if (err instanceof Error) message = err.message
|
||||||
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
|
else message = `Something went wrong, try again later!`
|
||||||
if (resHeader.ok) {
|
state.logs.push({
|
||||||
const resHeaderJson = await resHeader.json()
|
type: 'error',
|
||||||
const headerFiles: Record<string, { filename: string; content: string; language: string }> = {};
|
message: `Error: ${message}`
|
||||||
Object.entries(resHeaderJson).forEach(([key, value]) => {
|
})
|
||||||
const fname = `${key}.h`;
|
|
||||||
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' }
|
|
||||||
})
|
|
||||||
const files = {
|
|
||||||
...res.data.files,
|
|
||||||
...headerFiles
|
|
||||||
};
|
|
||||||
res.data.files = files;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return res;
|
|
||||||
// If you want to load templates from GIST instad, uncomment the code below and comment the code above.
|
|
||||||
// return octokit.request("GET /gists/{gist_id}", { gist_id: templateFileIds.headers }).then(({ data: { files: headerFiles } }) => {
|
|
||||||
// const files = { ...res.data.files, ...headerFiles }
|
|
||||||
// console.log(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 || "untitled.c",
|
|
||||||
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
|
|
||||||
content: res.data.files?.[filename]?.content || "",
|
|
||||||
}));
|
|
||||||
// Sort files so that the source files are first
|
|
||||||
// In case of other files leave the order as it its
|
|
||||||
files.sort((a, b) => {
|
|
||||||
const aBasename = a.name.split('.')?.[0];
|
|
||||||
const aCext = a.name?.toLowerCase().endsWith('.c');
|
|
||||||
const bBasename = b.name.split('.')?.[0];
|
|
||||||
const bCext = b.name?.toLowerCase().endsWith('.c');
|
|
||||||
// If a has c extension and b doesn't move a up
|
|
||||||
if (aCext && !bCext) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (!aCext && bCext) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
// Otherwise fallback to default sorting based on basename
|
|
||||||
if (aBasename > bBasename) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (bBasename > aBasename) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
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;
|
state.loading = false
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
import toast from "react-hot-toast";
|
import toast from 'react-hot-toast'
|
||||||
import { derive, XRPL_Account } from "xrpl-accountlib";
|
import { derive, XRPL_Account } from 'xrpl-accountlib'
|
||||||
|
|
||||||
import state from '../index';
|
import state from '../index'
|
||||||
import { names } from './addFaucetAccount';
|
import { names } from './addFaucetAccount'
|
||||||
|
|
||||||
// Adds test account to global state with secret key
|
// Adds test account to global state with secret key
|
||||||
export const importAccount = (secret: string) => {
|
export const importAccount = (secret: string, name?: string) => {
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
return toast.error("You need to add secret!");
|
return toast.error('You need to add secret!')
|
||||||
}
|
}
|
||||||
if (state.accounts.find((acc) => acc.secret === secret)) {
|
if (state.accounts.find(acc => acc.secret === secret)) {
|
||||||
return toast.error("Account already added!");
|
return toast.error('Account already added!')
|
||||||
}
|
}
|
||||||
let account: XRPL_Account | null = null;
|
let account: XRPL_Account | null = null
|
||||||
try {
|
try {
|
||||||
account = derive.familySeed(secret);
|
account = derive.familySeed(secret)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err?.message) {
|
if (err?.message) {
|
||||||
toast.error(err.message)
|
toast.error(err.message)
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error occured while importing account')
|
toast.error('Error occurred while importing account')
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (!account || !account.secret.familySeed) {
|
if (!account || !account.secret.familySeed) {
|
||||||
return toast.error(`Couldn't create account!`);
|
return toast.error(`Couldn't create account!`)
|
||||||
}
|
}
|
||||||
state.accounts.push({
|
state.accounts.push({
|
||||||
name: names[state.accounts.length],
|
name: name || names[state.accounts.length],
|
||||||
address: account.address || "",
|
address: account.address || '',
|
||||||
secret: account.secret.familySeed || "",
|
secret: account.secret.familySeed || '',
|
||||||
xrp: "0",
|
xrp: '0',
|
||||||
sequence: 1,
|
sequence: 1,
|
||||||
hooks: [],
|
hooks: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
version: '2'
|
version: '2'
|
||||||
});
|
})
|
||||||
return toast.success("Account imported successfully!");
|
return toast.success('Account imported successfully!')
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { addFaucetAccount } from "./addFaucetAccount";
|
import { addFaucetAccount } from './addFaucetAccount'
|
||||||
import { compileCode } from "./compileCode";
|
import { compileCode } from './compileCode'
|
||||||
import { createNewFile } from "./createNewFile";
|
import { createNewFile } from './createNewFile'
|
||||||
import { deployHook } from "./deployHook";
|
import { deployHook } from './deployHook'
|
||||||
import { fetchFiles } from "./fetchFiles";
|
import { fetchFiles } from './fetchFiles'
|
||||||
import { importAccount } from "./importAccount";
|
import { importAccount } from './importAccount'
|
||||||
import { saveFile } from "./saveFile";
|
import { saveFile } from './saveFile'
|
||||||
import { syncToGist } from "./syncToGist";
|
import { syncToGist } from './syncToGist'
|
||||||
import { updateEditorSettings } from "./updateEditorSettings";
|
import { updateEditorSettings } from './updateEditorSettings'
|
||||||
import { downloadAsZip } from "./downloadAsZip";
|
import { downloadAsZip } from './downloadAsZip'
|
||||||
import { sendTransaction } from "./sendTransaction";
|
import { sendTransaction } from './sendTransaction'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
addFaucetAccount,
|
addFaucetAccount,
|
||||||
@@ -22,4 +22,4 @@ export {
|
|||||||
updateEditorSettings,
|
updateEditorSettings,
|
||||||
downloadAsZip,
|
downloadAsZip,
|
||||||
sendTransaction
|
sendTransaction
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { snapshot } from "valtio"
|
import { snapshot } from 'valtio'
|
||||||
import state from ".."
|
import state from '..'
|
||||||
|
|
||||||
export type SplitSize = number[]
|
export type SplitSize = number[]
|
||||||
|
|
||||||
@@ -12,4 +12,3 @@ export const getSplit = (splitId: string): SplitSize | null => {
|
|||||||
const split = splits[splitId]
|
const split = splits[splitId]
|
||||||
return split ? split : null
|
return split ? split : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import toast from "react-hot-toast";
|
import toast from 'react-hot-toast'
|
||||||
import state from '../index';
|
import state from '../index'
|
||||||
|
|
||||||
// Saves the current editor content to global state
|
// Saves the current editor content to global state
|
||||||
export const saveFile = (showToast: boolean = true) => {
|
export const saveFile = (showToast: boolean = true, activeId?: number) => {
|
||||||
const editorModels = state.editorCtx?.getModels();
|
const editorModels = state.editorCtx?.getModels()
|
||||||
const sought = '/' + state.files[state.active].name;
|
const sought = '/' + state.files[state.active].name
|
||||||
const currentModel = editorModels?.find((editorModel) => {
|
const currentModel = editorModels?.find(editorModel => {
|
||||||
return editorModel.uri.path.endsWith(sought);
|
return editorModel.uri.path.endsWith(sought)
|
||||||
});
|
})
|
||||||
|
const file = state.files[activeId || state.active]
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
state.files[state.active].content = currentModel?.getValue() || "";
|
file.content = currentModel?.getValue() || ''
|
||||||
}
|
}
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
toast.success("Saved successfully", { position: "bottom-center" });
|
toast.success('Saved successfully', { position: 'bottom-center' })
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const saveAllFiles = () => {
|
export const saveAllFiles = () => {
|
||||||
const editorModels = state.editorCtx?.getModels();
|
const editorModels = state.editorCtx?.getModels()
|
||||||
state.files.forEach(file => {
|
state.files.forEach(file => {
|
||||||
const currentModel = editorModels?.find(model => model.uri.path.endsWith('/' + file.name))
|
const currentModel = editorModels?.find(model => model.uri.path.endsWith('/' + file.name))
|
||||||
if (currentModel) {
|
if (currentModel) {
|
||||||
file.content = currentModel?.getValue() || '';
|
file.content = currentModel?.getValue() || ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
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,
|
|
||||||
Fee, // TODO auto-fillable default
|
|
||||||
...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}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const currAcc = state.accounts.find(acc => acc.address === account.address);
|
|
||||||
if (currAcc && response.account_sequence_next) {
|
|
||||||
currAcc.sequence = response.account_sequence_next;
|
|
||||||
}
|
|
||||||
} 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`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
81
state/actions/sendTransaction.tsx
Normal file
81
state/actions/sendTransaction.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { derive, sign } from 'xrpl-accountlib'
|
||||||
|
|
||||||
|
import state from '..'
|
||||||
|
import type { IAccount } from '..'
|
||||||
|
import ResultLink from '../../components/ResultLink'
|
||||||
|
import { ref } from 'valtio'
|
||||||
|
import { xrplSend } from './xrpl-client'
|
||||||
|
|
||||||
|
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
|
||||||
|
) => {
|
||||||
|
const { Fee = '1000', ...opts } = txOptions
|
||||||
|
const tx: TransactionOptions = {
|
||||||
|
Account: account.address,
|
||||||
|
Sequence: account.sequence,
|
||||||
|
Fee,
|
||||||
|
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
|
||||||
|
...opts
|
||||||
|
}
|
||||||
|
const { logPrefix = '' } = options || {}
|
||||||
|
state.transactionLogs.push({
|
||||||
|
type: 'log',
|
||||||
|
message: `${logPrefix}${JSON.stringify(tx, null, 2)}`
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const signedAccount = derive.familySeed(account.secret)
|
||||||
|
const { signedTransaction } = sign(tx, signedAccount)
|
||||||
|
const response = await xrplSend({
|
||||||
|
command: 'submit',
|
||||||
|
tx_blob: signedTransaction
|
||||||
|
})
|
||||||
|
|
||||||
|
const resultMsg = ref(
|
||||||
|
<>
|
||||||
|
{logPrefix}[<ResultLink result={response.engine_result} />] {response.engine_result_message}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
if (response.engine_result === 'tesSUCCESS') {
|
||||||
|
state.transactionLogs.push({
|
||||||
|
type: 'success',
|
||||||
|
message: resultMsg
|
||||||
|
})
|
||||||
|
} else if (response.engine_result) {
|
||||||
|
state.transactionLogs.push({
|
||||||
|
type: 'error',
|
||||||
|
message: resultMsg
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
state.transactionLogs.push({
|
||||||
|
type: 'error',
|
||||||
|
message: `${logPrefix}[${response.error}] ${response.error_exception}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const currAcc = state.accounts.find(acc => acc.address === account.address)
|
||||||
|
if (currAcc && response.account_sequence_next) {
|
||||||
|
currAcc.sequence = response.account_sequence_next
|
||||||
|
}
|
||||||
|
} 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`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
import { ref } from 'valtio';
|
import { ref } from 'valtio'
|
||||||
import { AlertState, alertState } from "../../components/AlertDialog";
|
import { AlertState, alertState } from '../../components/AlertDialog'
|
||||||
|
|
||||||
export const showAlert = (title: string, opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}) => {
|
export const showAlert = (
|
||||||
const { body: _body, confirmPrefix: _confirmPrefix, ...rest } = opts
|
title: string,
|
||||||
const body = (_body && typeof _body === 'object') ? ref(_body) : _body
|
opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}
|
||||||
const confirmPrefix = (_confirmPrefix && typeof _confirmPrefix === 'object') ? ref(_confirmPrefix) : _confirmPrefix
|
) => {
|
||||||
|
const { body: _body, confirmPrefix: _confirmPrefix, ...rest } = opts
|
||||||
|
const body = _body && typeof _body === 'object' ? ref(_body) : _body
|
||||||
|
const confirmPrefix =
|
||||||
|
_confirmPrefix && typeof _confirmPrefix === 'object' ? ref(_confirmPrefix) : _confirmPrefix
|
||||||
|
|
||||||
const nwState: AlertState = {
|
const nwState: AlertState = {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
confirmPrefix,
|
confirmPrefix,
|
||||||
cancelText: undefined,
|
cancelText: undefined,
|
||||||
confirmText: undefined,
|
confirmText: undefined,
|
||||||
onCancel: undefined,
|
onCancel: undefined,
|
||||||
onConfirm: undefined,
|
onConfirm: undefined,
|
||||||
...rest,
|
...rest
|
||||||
}
|
}
|
||||||
Object.entries(nwState).forEach(([key, value]) => {
|
Object.entries(nwState).forEach(([key, value]) => {
|
||||||
(alertState as any)[key] = value
|
;(alertState as any)[key] = value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,97 @@
|
|||||||
import type { Session } from "next-auth";
|
import type { Session } from 'next-auth'
|
||||||
import toast from "react-hot-toast";
|
import toast from 'react-hot-toast'
|
||||||
import { Octokit } from "@octokit/core";
|
import { Octokit } from '@octokit/core'
|
||||||
import Router from "next/router";
|
import Router from 'next/router'
|
||||||
|
|
||||||
import state from '../index';
|
import state from '../index'
|
||||||
import { saveAllFiles } from "./saveFile";
|
import { saveAllFiles } from './saveFile'
|
||||||
|
|
||||||
const octokit = new Octokit();
|
const octokit = new Octokit()
|
||||||
|
|
||||||
// Syncs the current files from the state to GitHub Gists.
|
// Syncs the current files from the state to GitHub Gists.
|
||||||
export const syncToGist = async (
|
export const syncToGist = async (session?: Session | null, createNewGist?: boolean) => {
|
||||||
session?: Session | null,
|
saveAllFiles()
|
||||||
createNewGist?: boolean
|
let files: Record<string, { filename: string; content: string }> = {}
|
||||||
) => {
|
state.gistLoading = true
|
||||||
saveAllFiles();
|
|
||||||
let files: Record<string, { filename: string; content: string }> = {};
|
|
||||||
state.gistLoading = true;
|
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
state.gistLoading = false;
|
state.gistLoading = false
|
||||||
return toast.error("You need to be logged in!");
|
return toast.error('You need to be logged in!')
|
||||||
}
|
}
|
||||||
const toastId = toast.loading("Pushing to Gist");
|
const toastId = toast.loading('Pushing to Gist')
|
||||||
if (!state.files || !state.files.length) {
|
if (!state.files || !state.files.length) {
|
||||||
state.gistLoading = false;
|
state.gistLoading = false
|
||||||
return toast.error(`You need to create some files we can push to gist`, {
|
return toast.error(`You need to create some files we can push to gist`, {
|
||||||
id: toastId,
|
id: toastId
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
if (
|
if (state.gistId && session?.user.username === state.gistOwner && !createNewGist) {
|
||||||
state.gistId &&
|
|
||||||
session?.user.username === state.gistOwner &&
|
|
||||||
!createNewGist
|
|
||||||
) {
|
|
||||||
// You can only remove files from Gist by updating file with empty contents
|
// 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
|
// 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
|
// and then send empty content if we don't have matching files anymore
|
||||||
// on local state
|
// on local state
|
||||||
const currentFilesRes = await octokit.request("GET /gists/{gist_id}", {
|
const currentFilesRes = await octokit.request('GET /gists/{gist_id}', {
|
||||||
gist_id: state.gistId,
|
gist_id: state.gistId
|
||||||
});
|
})
|
||||||
if (currentFilesRes.data.files) {
|
if (currentFilesRes.data.files) {
|
||||||
Object.keys(currentFilesRes?.data?.files).forEach((filename) => {
|
Object.keys(currentFilesRes?.data?.files).forEach(filename => {
|
||||||
files[`${filename}`] = { filename, content: "" };
|
files[`${filename}`] = { filename, content: '' }
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
state.files.forEach((file) => {
|
state.files.forEach(file => {
|
||||||
files[`${file.name}`] = { filename: file.name, content: file.content };
|
files[`${file.name}`] = { filename: file.name, content: file.content }
|
||||||
});
|
})
|
||||||
// Update existing Gist
|
// Update existing Gist
|
||||||
octokit
|
octokit
|
||||||
.request("PATCH /gists/{gist_id}", {
|
.request('PATCH /gists/{gist_id}', {
|
||||||
gist_id: state.gistId,
|
gist_id: state.gistId,
|
||||||
files,
|
files,
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `token ${session?.accessToken || ""}`,
|
authorization: `token ${session?.accessToken || ''}`
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then(res => {
|
||||||
state.gistLoading = false;
|
state.gistLoading = false
|
||||||
return toast.success("Updated to gist successfully!", { id: toastId });
|
return toast.success('Updated to gist successfully!', { id: toastId })
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
console.log(err);
|
console.log(err)
|
||||||
state.gistLoading = false;
|
state.gistLoading = false
|
||||||
return toast.error(`Could not update Gist, try again later!`, {
|
return toast.error(`Could not update Gist, try again later!`, {
|
||||||
id: toastId,
|
id: toastId
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Not Gist of the current user or it isn't Gist yet
|
// Not Gist of the current user or it isn't Gist yet
|
||||||
state.files.forEach((file) => {
|
state.files.forEach(file => {
|
||||||
files[`${file.name}`] = { filename: file.name, content: file.content };
|
files[`${file.name}`] = { filename: file.name, content: file.content }
|
||||||
});
|
})
|
||||||
octokit
|
octokit
|
||||||
.request("POST /gists", {
|
.request('POST /gists', {
|
||||||
files,
|
files,
|
||||||
public: true,
|
public: true,
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `token ${session?.accessToken || ""}`,
|
authorization: `token ${session?.accessToken || ''}`
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then(res => {
|
||||||
state.gistLoading = false;
|
state.gistLoading = false
|
||||||
state.gistOwner = res.data.owner?.login;
|
state.gistOwner = res.data.owner?.login
|
||||||
state.gistId = res.data.id;
|
state.gistId = res.data.id
|
||||||
state.gistName = Array.isArray(res.data.files)
|
state.gistName = Array.isArray(res.data.files)
|
||||||
? Object.keys(res.data?.files)?.[0]
|
? Object.keys(res.data?.files)?.[0]
|
||||||
: "Untitled";
|
: 'Untitled'
|
||||||
Router.push({ pathname: `/develop/${res.data.id}` });
|
Router.push({ pathname: `/develop/${res.data.id}` })
|
||||||
return toast.success("Created new gist successfully!", { id: toastId });
|
return toast.success('Created new gist successfully!', { id: toastId })
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
console.log(err);
|
console.log(err)
|
||||||
state.gistLoading = false;
|
state.gistLoading = false
|
||||||
return toast.error(`Could not create Gist, try again later!`, {
|
return toast.error(`Could not create Gist, try again later!`, {
|
||||||
id: toastId,
|
id: toastId
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default syncToGist;
|
export default syncToGist
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import state, { IState } from '../index';
|
import state, { IState } from '../index'
|
||||||
|
|
||||||
// Updates editor settings and stores them
|
// Updates editor settings and stores them
|
||||||
// in global state
|
// in global state
|
||||||
export const updateEditorSettings = (
|
export const updateEditorSettings = (editorSettings: IState['editorSettings']) => {
|
||||||
editorSettings: IState["editorSettings"]
|
state.editorCtx?.getModels().forEach(model => {
|
||||||
) => {
|
|
||||||
state.editorCtx?.getModels().forEach((model) => {
|
|
||||||
model.updateOptions({
|
model.updateOptions({
|
||||||
...editorSettings,
|
...editorSettings
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
return (state.editorSettings = editorSettings);
|
return (state.editorSettings = editorSettings)
|
||||||
};
|
}
|
||||||
|
|||||||
7
state/actions/xrpl-client.ts
Normal file
7
state/actions/xrpl-client.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { XrplClient } from 'xrpl-client';
|
||||||
|
import state from '..';
|
||||||
|
|
||||||
|
export const xrplSend = async(...params: Parameters<XrplClient['send']>) => {
|
||||||
|
const client = await state.client.ready()
|
||||||
|
return client.send(...params);
|
||||||
|
}
|
||||||
79
state/constants/flags.ts
Normal file
79
state/constants/flags.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { SelectOption } from '../transactions';
|
||||||
|
|
||||||
|
interface Flags {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transactionFlags: { [key: /* TransactionType */ string]: Flags } = {
|
||||||
|
"*": {
|
||||||
|
tfFullyCanonicalSig: '0x80000000'
|
||||||
|
},
|
||||||
|
Payment: {
|
||||||
|
tfNoDirectRipple: '0x00010000',
|
||||||
|
tfPartialPayment: '0x00020000',
|
||||||
|
tfLimitQuality: '0x00040000',
|
||||||
|
},
|
||||||
|
AccountSet: {
|
||||||
|
tfRequireDestTag: '0x00010000',
|
||||||
|
tfOptionalDestTag: '0x00020000',
|
||||||
|
tfRequireAuth: '0x00040000',
|
||||||
|
tfOptionalAuth: '0x00080000',
|
||||||
|
tfDisallowXRP: '0x00100000',
|
||||||
|
tfAllowXRP: '0x00200000',
|
||||||
|
},
|
||||||
|
NFTokenCreateOffer: {
|
||||||
|
tfSellNFToken: '0x00000001',
|
||||||
|
},
|
||||||
|
NFTokenMint: {
|
||||||
|
tfBurnable: '0x00000001',
|
||||||
|
tfOnlyXRP: '0x00000002',
|
||||||
|
tfTrustLine: '0x00000004',
|
||||||
|
tfTransferable: '0x00000008',
|
||||||
|
},
|
||||||
|
OfferCreate: {
|
||||||
|
tfPassive: '0x00010000',
|
||||||
|
tfImmediateOrCancel: '0x00020000',
|
||||||
|
tfFillOrKill: '0x00040000',
|
||||||
|
tfSell: '0x00080000',
|
||||||
|
},
|
||||||
|
PaymentChannelClaim: {
|
||||||
|
tfRenew: '0x00010000',
|
||||||
|
tfClose: '0x00020000',
|
||||||
|
},
|
||||||
|
TrustSet: {
|
||||||
|
tfSetfAuth: '0x00010000',
|
||||||
|
tfSetNoRipple: '0x00020000',
|
||||||
|
tfClearNoRipple: '0x00040000',
|
||||||
|
tfSetFreeze: '0x00100000',
|
||||||
|
tfClearFreeze: '0x00200000',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFlags = (tt?: string) => {
|
||||||
|
if (!tt) return
|
||||||
|
const flags = {
|
||||||
|
...transactionFlags['*'],
|
||||||
|
...transactionFlags[tt]
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function combineFlags(flags?: string[]): string | undefined {
|
||||||
|
if (!flags) return
|
||||||
|
|
||||||
|
const num = flags.reduce((cumm, curr) => cumm | BigInt(curr), BigInt(0))
|
||||||
|
return num.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractFlags(transactionType: string, flags?: string | number,): SelectOption[] {
|
||||||
|
const flagsObj = getFlags(transactionType)
|
||||||
|
if (!flags || !flagsObj) return []
|
||||||
|
|
||||||
|
const extracted = Object.entries(flagsObj).reduce((cumm, [label, value]) => {
|
||||||
|
return (BigInt(flags) & BigInt(value)) ? cumm.concat({ label, value }) : cumm
|
||||||
|
}, [] as SelectOption[])
|
||||||
|
|
||||||
|
return extracted
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
export * from './templates'
|
export * from './templates'
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
// export const templateFileIds = {
|
import Carbon from '../../components/icons/Carbon'
|
||||||
// 'starter': '1d14e51e2e02dc0a508cb0733767a914', // TODO currently same as accept
|
import Firewall from '../../components/icons/Firewall'
|
||||||
// 'firewall': 'bcd6d0c0fcbe52545ddb802481ff9d26',
|
import Notary from '../../components/icons/Notary'
|
||||||
// 'notary': 'a789c75f591eeab7932fd702ed8cf9ea',
|
import Peggy from '../../components/icons/Peggy'
|
||||||
// 'carbon': '43925143fa19735d8c6505c34d3a6a47',
|
import Starter from '../../components/icons/Starter'
|
||||||
// 'peggy': 'ceaf352e2a65741341033ab7ef05c448',
|
|
||||||
// 'headers': '9b448e8a55fab11ef5d1274cb59f9cf3'
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const templateFileIds = {
|
export const templateFileIds = {
|
||||||
'starter': '1f7d2963d9e342ea092286115274f3e3',
|
starter: {
|
||||||
'firewall': '70edec690f0de4dd315fad1f4f996d8c',
|
id: '1f8109c80f504e6326db2735df2f0ad6', // Forked
|
||||||
'notary': '3d5677768fe8a54c4f6317e185d9ba66',
|
name: 'Starter',
|
||||||
'carbon': 'a9fbcaf1b816b198c7fc0f62962bebf2',
|
description:
|
||||||
'doubler': '56b86174aeb70b2b48eee962bad3e355',
|
'Just a basic starter with essential imports, just accepts any transaction coming through',
|
||||||
'peggy': 'd21298a37e1550b781682014762a567b',
|
icon: Starter
|
||||||
'headers': '55f639bce59a49c58c45e663776b5138'
|
},
|
||||||
|
firewall: {
|
||||||
|
id: '1cc30f39c8a0b9c55b88c312669ca45e', // Forked
|
||||||
|
name: 'Firewall',
|
||||||
|
description: 'This Hook essentially checks a blacklist of accounts',
|
||||||
|
icon: Firewall
|
||||||
|
},
|
||||||
|
notary: {
|
||||||
|
id: '87b6f5a8c2f5038fb0f20b8b510efa10', // Forked
|
||||||
|
name: 'Notary',
|
||||||
|
description: 'Collecting signatures for multi-sign transactions',
|
||||||
|
icon: Notary
|
||||||
|
},
|
||||||
|
carbon: {
|
||||||
|
id: '953662b22d065449f8ab6f69bc2afe41', // Forked
|
||||||
|
name: 'Carbon',
|
||||||
|
description: 'Send a percentage of sum to an address',
|
||||||
|
icon: Carbon
|
||||||
|
},
|
||||||
|
peggy: {
|
||||||
|
id: '049784a83fa068faf7912f663f7b6471', // Forked
|
||||||
|
name: 'Peggy',
|
||||||
|
description: 'An oracle based stable coin hook',
|
||||||
|
icon: Peggy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'hookmacro.h']
|
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'macro.h', 'extern.h', 'error.h']
|
||||||
|
|||||||
202
state/index.ts
202
state/index.ts
@@ -1,88 +1,93 @@
|
|||||||
import type monaco from "monaco-editor";
|
import type monaco from 'monaco-editor'
|
||||||
import { proxy, ref, subscribe } from "valtio";
|
import { proxy, ref, subscribe } from 'valtio'
|
||||||
import { devtools } from 'valtio/utils';
|
import { devtools, subscribeKey } from 'valtio/utils'
|
||||||
import { XrplClient } from "xrpl-client";
|
import { XrplClient } from 'xrpl-client'
|
||||||
import { SplitSize } from "./actions/persistSplits";
|
import { SplitSize } from './actions/persistSplits'
|
||||||
|
|
||||||
declare module "valtio" {
|
declare module 'valtio' {
|
||||||
function useSnapshot<T extends object>(p: T): T;
|
function useSnapshot<T extends object>(p: T): T
|
||||||
function snapshot<T extends object>(p: T): T;
|
function snapshot<T extends object>(p: T): T
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFile {
|
export interface IFile {
|
||||||
name: string;
|
name: string
|
||||||
language: string;
|
language: string
|
||||||
content: string;
|
content: string
|
||||||
compiledContent?: ArrayBuffer | null;
|
compiledValueSnapshot?: string
|
||||||
compiledWatContent?: string | null;
|
compiledContent?: ArrayBuffer | null
|
||||||
|
compiledWatContent?: string | null
|
||||||
lastCompiled?: Date
|
lastCompiled?: Date
|
||||||
|
containsErrors?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FaucetAccountRes {
|
export interface FaucetAccountRes {
|
||||||
address: string;
|
address: string
|
||||||
secret: string;
|
secret: string
|
||||||
xrp: number;
|
xrp: number
|
||||||
hash: string;
|
hash: string
|
||||||
code: string;
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccount {
|
export interface IAccount {
|
||||||
name: string;
|
name: string
|
||||||
address: string;
|
address: string
|
||||||
secret: string;
|
secret: string
|
||||||
xrp: string;
|
xrp: string
|
||||||
sequence: number;
|
sequence: number
|
||||||
hooks: string[];
|
hooks: string[]
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
version?: string;
|
version?: string
|
||||||
error?: {
|
error?: {
|
||||||
message: string;
|
message: string
|
||||||
code: string;
|
code: string
|
||||||
} | null;
|
} | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILog {
|
export interface ILog {
|
||||||
type: "error" | "warning" | "log" | "success";
|
type: 'error' | 'warning' | 'log' | 'success'
|
||||||
message: string | JSX.Element;
|
message: string | JSX.Element
|
||||||
key?: string;
|
key?: string
|
||||||
jsonData?: any,
|
jsonData?: any
|
||||||
timestring?: string;
|
timestring?: string
|
||||||
link?: string;
|
link?: string
|
||||||
linkText?: string;
|
linkText?: string
|
||||||
defaultCollapsed?: boolean
|
defaultCollapsed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeployValue = Record<IFile['name'], any>
|
||||||
|
|
||||||
export interface IState {
|
export interface IState {
|
||||||
files: IFile[];
|
files: IFile[]
|
||||||
gistId?: string | null;
|
gistId?: string | null
|
||||||
gistOwner?: string | null;
|
gistOwner?: string | null
|
||||||
gistName?: string | null;
|
gistName?: string | null
|
||||||
active: number;
|
active: number
|
||||||
activeWat: number;
|
activeWat: number
|
||||||
loading: boolean;
|
loading: boolean
|
||||||
gistLoading: boolean;
|
gistLoading: boolean
|
||||||
zipLoading: boolean;
|
zipLoading: boolean
|
||||||
compiling: boolean;
|
compiling: boolean
|
||||||
logs: ILog[];
|
logs: ILog[]
|
||||||
deployLogs: ILog[];
|
deployLogs: ILog[]
|
||||||
transactionLogs: ILog[];
|
transactionLogs: ILog[]
|
||||||
scriptLogs: ILog[];
|
scriptLogs: ILog[]
|
||||||
editorCtx?: typeof monaco.editor;
|
editorCtx?: typeof monaco.editor
|
||||||
editorSettings: {
|
editorSettings: {
|
||||||
tabSize: number;
|
tabSize: number
|
||||||
};
|
}
|
||||||
splits: {
|
splits: {
|
||||||
[id: string]: SplitSize
|
[id: string]: SplitSize
|
||||||
};
|
}
|
||||||
client: XrplClient | null;
|
client: XrplClient
|
||||||
clientStatus: "offline" | "online";
|
clientStatus: 'offline' | 'online'
|
||||||
mainModalOpen: boolean;
|
mainModalOpen: boolean
|
||||||
mainModalShowed: boolean;
|
mainModalShowed: boolean
|
||||||
accounts: IAccount[];
|
accounts: IAccount[]
|
||||||
compileOptions: {
|
compileOptions: {
|
||||||
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
|
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os'
|
||||||
strip: boolean
|
strip: boolean
|
||||||
}
|
}
|
||||||
|
deployValues: DeployValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// let localStorageState: null | string = null;
|
// let localStorageState: null | string = null;
|
||||||
@@ -105,35 +110,36 @@ let initialState: IState = {
|
|||||||
gistLoading: false,
|
gistLoading: false,
|
||||||
zipLoading: false,
|
zipLoading: false,
|
||||||
editorSettings: {
|
editorSettings: {
|
||||||
tabSize: 2,
|
tabSize: 2
|
||||||
},
|
},
|
||||||
splits: {},
|
splits: {},
|
||||||
client: null,
|
client: undefined!, // set below only.
|
||||||
clientStatus: "offline" as "offline",
|
clientStatus: 'offline' as 'offline',
|
||||||
mainModalOpen: false,
|
mainModalOpen: false,
|
||||||
mainModalShowed: false,
|
mainModalShowed: false,
|
||||||
accounts: [],
|
accounts: [],
|
||||||
compileOptions: {
|
compileOptions: {
|
||||||
optimizationLevel: '-O0',
|
optimizationLevel: '-O2',
|
||||||
strip: true
|
strip: true
|
||||||
}
|
},
|
||||||
};
|
deployValues: {}
|
||||||
|
}
|
||||||
|
|
||||||
let localStorageAccounts: string | null = null;
|
let localStorageAccounts: string | null = null
|
||||||
let initialAccounts: IAccount[] = [];
|
let initialAccounts: IAccount[] = []
|
||||||
|
|
||||||
// TODO: What exactly should we store in localStorage? editorSettings, splits, accounts?
|
// TODO: What exactly should we store in localStorage? editorSettings, splits, accounts?
|
||||||
|
|
||||||
// Check if there's a persited accounts in localStorage
|
// Check if there's a persited accounts in localStorage
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
localStorageAccounts = localStorage.getItem("hooksIdeAccounts");
|
localStorageAccounts = localStorage.getItem('hooksIdeAccounts')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`localStorage state broken`);
|
console.log(`localStorage state broken`)
|
||||||
localStorage.removeItem("hooksIdeAccounts");
|
localStorage.removeItem('hooksIdeAccounts')
|
||||||
}
|
}
|
||||||
if (localStorageAccounts) {
|
if (localStorageAccounts) {
|
||||||
initialAccounts = JSON.parse(localStorageAccounts);
|
initialAccounts = JSON.parse(localStorageAccounts)
|
||||||
}
|
}
|
||||||
// filter out old accounts (they do not have version property at all)
|
// filter out old accounts (they do not have version property at all)
|
||||||
// initialAccounts = initialAccounts.filter(acc => acc.version === '2');
|
// initialAccounts = initialAccounts.filter(acc => acc.version === '2');
|
||||||
@@ -143,35 +149,41 @@ if (typeof window !== "undefined") {
|
|||||||
const state = proxy<IState>({
|
const state = proxy<IState>({
|
||||||
...initialState,
|
...initialState,
|
||||||
accounts: initialAccounts.length > 0 ? initialAccounts : [],
|
accounts: initialAccounts.length > 0 ? initialAccounts : [],
|
||||||
logs: [],
|
logs: []
|
||||||
});
|
})
|
||||||
// Initialize socket connection
|
// Initialize socket connection
|
||||||
const client = new XrplClient(`wss://${process.env.NEXT_PUBLIC_TESTNET_URL}`);
|
const client = new XrplClient(`wss://${process.env.NEXT_PUBLIC_TESTNET_URL}`)
|
||||||
|
state.client = ref(client);
|
||||||
|
|
||||||
client.on("online", () => {
|
client.on('online', () => {
|
||||||
state.client = ref(client);
|
state.clientStatus = 'online'
|
||||||
state.clientStatus = "online";
|
})
|
||||||
});
|
|
||||||
|
|
||||||
client.on("offline", () => {
|
client.on('offline', () => {
|
||||||
state.clientStatus = "offline";
|
state.clientStatus = 'offline'
|
||||||
});
|
})
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
devtools(state, "Files State");
|
devtools(state, 'Files State')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== 'undefined') {
|
||||||
subscribe(state, () => {
|
subscribe(state.accounts, () => {
|
||||||
const { accounts, active } = state;
|
const { accounts } = state
|
||||||
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
|
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
|
||||||
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
|
localStorage.setItem('hooksIdeAccounts', JSON.stringify(accountsNoLoading))
|
||||||
if (!state.files[active]?.compiledWatContent) {
|
})
|
||||||
state.activeWat = 0;
|
|
||||||
} else {
|
const updateActiveWat = () => {
|
||||||
state.activeWat = active;
|
const filename = state.files[state.active]?.name
|
||||||
}
|
|
||||||
});
|
const compiledFiles = state.files.filter(file => file.compiledContent)
|
||||||
|
const idx = compiledFiles.findIndex(file => file.name === filename)
|
||||||
|
|
||||||
|
if (idx !== -1) state.activeWat = idx
|
||||||
|
}
|
||||||
|
subscribeKey(state, 'active', updateActiveWat)
|
||||||
|
subscribeKey(state, 'files', updateActiveWat)
|
||||||
}
|
}
|
||||||
export default state
|
export default state
|
||||||
|
|
||||||
|
|||||||
@@ -1,246 +1,299 @@
|
|||||||
import { proxy } from 'valtio';
|
import { proxy } from 'valtio'
|
||||||
import { deepEqual } from '../utils/object';
|
import { deepEqual } from '../utils/object'
|
||||||
import transactionsData from "../content/transactions.json";
|
import transactionsData from '../content/transactions.json'
|
||||||
import state from '.';
|
import state from '.'
|
||||||
import { showAlert } from "../state/actions/showAlert";
|
import { showAlert } from '../state/actions/showAlert'
|
||||||
import { parseJSON } from '../utils/json';
|
import { parseJSON } from '../utils/json'
|
||||||
|
import { extractFlags, getFlags } from './constants/flags'
|
||||||
|
import { fromHex } from '../utils/setHook'
|
||||||
|
|
||||||
export type SelectOption = {
|
export type SelectOption = {
|
||||||
value: string;
|
value: string
|
||||||
label: string;
|
label: string
|
||||||
};
|
|
||||||
|
|
||||||
export interface TransactionState {
|
|
||||||
selectedTransaction: SelectOption | null;
|
|
||||||
selectedAccount: SelectOption | null;
|
|
||||||
selectedDestAccount: SelectOption | null;
|
|
||||||
txIsLoading: boolean;
|
|
||||||
txIsDisabled: boolean;
|
|
||||||
txFields: TxFields;
|
|
||||||
viewType: 'json' | 'ui',
|
|
||||||
editorSavedValue: null | string,
|
|
||||||
editorValue?: string,
|
|
||||||
estimatedFee?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HookParameters = {
|
||||||
|
[key: string]: SelectOption
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Memos = {
|
||||||
|
[key: string]: {
|
||||||
|
type: string
|
||||||
|
format: string
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionState {
|
||||||
|
selectedTransaction: SelectOption | null
|
||||||
|
selectedAccount: SelectOption | null
|
||||||
|
selectedDestAccount: SelectOption | null
|
||||||
|
selectedFlags: SelectOption[] | null
|
||||||
|
hookParameters: HookParameters
|
||||||
|
memos: Memos
|
||||||
|
txIsLoading: boolean
|
||||||
|
txIsDisabled: boolean
|
||||||
|
txFields: TxFields
|
||||||
|
viewType: 'json' | 'ui'
|
||||||
|
editorValue?: string
|
||||||
|
editorIsSaved: boolean
|
||||||
|
estimatedFee?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonFields = ['TransactionType', 'Account', 'Sequence', "HookParameters"] as const;
|
||||||
|
|
||||||
export type TxFields = Omit<
|
export type TxFields = Omit<
|
||||||
typeof transactionsData[0],
|
Partial<typeof transactionsData[0]>,
|
||||||
"Account" | "Sequence" | "TransactionType"
|
typeof commonFields[number]
|
||||||
>;
|
>
|
||||||
|
|
||||||
export const defaultTransaction: TransactionState = {
|
export const defaultTransaction: TransactionState = {
|
||||||
selectedTransaction: null,
|
selectedTransaction: null,
|
||||||
selectedAccount: null,
|
selectedAccount: null,
|
||||||
selectedDestAccount: null,
|
selectedDestAccount: null,
|
||||||
txIsLoading: false,
|
selectedFlags: null,
|
||||||
txIsDisabled: false,
|
hookParameters: {},
|
||||||
txFields: {},
|
memos: {},
|
||||||
viewType: 'ui',
|
editorIsSaved: true,
|
||||||
editorSavedValue: null
|
txIsLoading: false,
|
||||||
};
|
txIsDisabled: false,
|
||||||
|
txFields: {},
|
||||||
|
viewType: 'ui'
|
||||||
|
}
|
||||||
|
|
||||||
export const transactionsState = proxy({
|
export const transactionsState = proxy({
|
||||||
transactions: [
|
transactions: [
|
||||||
{
|
{
|
||||||
header: "test1.json",
|
header: 'test1.json',
|
||||||
state: defaultTransaction,
|
state: { ...defaultTransaction }
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
activeHeader: "test1.json"
|
activeHeader: 'test1.json'
|
||||||
});
|
})
|
||||||
|
|
||||||
|
export const renameTxState = (oldName: string, nwName: string) => {
|
||||||
|
const tx = transactionsState.transactions.find(tx => tx.header === oldName)
|
||||||
|
|
||||||
|
if (!tx) throw Error(`No transaction state exists with given header name ${oldName}`)
|
||||||
|
|
||||||
|
tx.header = nwName
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple transaction state changer
|
* Simple transaction state changer
|
||||||
* @param header Unique key and tab name for the transaction tab
|
* @param header Unique key and tab name for the transaction tab
|
||||||
* @param partialTx partial transaction state, `undefined` deletes the transaction
|
* @param partialTx partial transaction state, `undefined` deletes the transaction
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const modifyTransaction = (
|
export const modifyTxState = (
|
||||||
header: string,
|
header: string,
|
||||||
partialTx?: Partial<TransactionState>,
|
partialTx?: Partial<TransactionState>,
|
||||||
opts: { replaceState?: boolean } = {}
|
opts: { replaceState?: boolean } = {}
|
||||||
) => {
|
) => {
|
||||||
const tx = transactionsState.transactions.find(tx => tx.header === header);
|
const tx = transactionsState.transactions.find(tx => tx.header === header)
|
||||||
|
|
||||||
if (partialTx === undefined) {
|
if (partialTx === undefined) {
|
||||||
transactionsState.transactions = transactionsState.transactions.filter(
|
transactionsState.transactions = transactionsState.transactions.filter(
|
||||||
tx => tx.header !== header
|
tx => tx.header !== header
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tx) {
|
||||||
|
const state = {
|
||||||
|
...defaultTransaction,
|
||||||
|
...partialTx
|
||||||
}
|
}
|
||||||
|
transactionsState.transactions.push({
|
||||||
|
header,
|
||||||
|
state
|
||||||
|
})
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
if (!tx) {
|
if (opts.replaceState) {
|
||||||
const state = {
|
const repTx: TransactionState = {
|
||||||
...defaultTransaction,
|
...defaultTransaction,
|
||||||
...partialTx,
|
...partialTx
|
||||||
}
|
|
||||||
transactionsState.transactions.push({
|
|
||||||
header,
|
|
||||||
state,
|
|
||||||
});
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
tx.state = repTx
|
||||||
|
return repTx
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.replaceState) {
|
Object.keys(partialTx).forEach(k => {
|
||||||
const repTx: TransactionState = {
|
// Typescript mess here, but is definitely safe!
|
||||||
...defaultTransaction,
|
const s = tx.state as any
|
||||||
...partialTx,
|
const p = partialTx as any // ? Make copy
|
||||||
}
|
if (!deepEqual(s[k], p[k])) s[k] = p[k]
|
||||||
tx.state = repTx
|
})
|
||||||
return repTx
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(partialTx).forEach(k => {
|
return tx.state
|
||||||
// Typescript mess here, but is definetly safe!
|
}
|
||||||
const s = tx.state as any;
|
|
||||||
const p = partialTx as any; // ? Make copy
|
|
||||||
if (!deepEqual(s[k], p[k])) s[k] = p[k];
|
|
||||||
});
|
|
||||||
|
|
||||||
return tx.state
|
|
||||||
};
|
|
||||||
|
|
||||||
// state to tx options
|
// state to tx options
|
||||||
export const prepareTransaction = (data: any) => {
|
export const prepareTransaction = (data: any) => {
|
||||||
let options = { ...data };
|
let options = { ...data }
|
||||||
|
|
||||||
(Object.keys(options)).forEach(field => {
|
Object.keys(options).forEach(field => {
|
||||||
let _value = options[field];
|
let _value = options[field]
|
||||||
// convert xrp
|
// convert xrp
|
||||||
if (_value && typeof _value === "object" && _value.$type === "xrp") {
|
if (_value && typeof _value === 'object' && _value.$type === 'xrp') {
|
||||||
if (+_value.$value) {
|
if (+_value.$value) {
|
||||||
options[field] = (+_value.$value * 1000000 + "") as any;
|
options[field] = (+_value.$value * 1000000 + '') as any
|
||||||
} else {
|
} else {
|
||||||
options[field] = undefined; // 👇 💀
|
options[field] = undefined // 👇 💀
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// handle type: `json`
|
// handle type: `json`
|
||||||
if (_value && typeof _value === "object" && _value.$type === "json") {
|
if (_value && typeof _value === 'object' && _value.$type === 'json') {
|
||||||
if (typeof _value.$value === "object") {
|
if (typeof _value.$value === 'object') {
|
||||||
options[field] = _value.$value as any;
|
options[field] = _value.$value
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
options[field] = JSON.parse(_value.$value);
|
options[field] = JSON.parse(_value.$value)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ""
|
const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ''
|
||||||
}`;
|
}`
|
||||||
console.error(message)
|
console.error(message)
|
||||||
options[field] = _value.$value
|
options[field] = _value.$value
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// delete unneccesary fields
|
// delete unnecessary fields
|
||||||
if (options[field] === undefined) {
|
if (!options[field]) {
|
||||||
delete options[field];
|
delete options[field]
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// editor value to state
|
// editor value to state
|
||||||
export const prepareState = (value: string, transactionType?: string) => {
|
export const prepareState = (value: string, transactionType?: string) => {
|
||||||
const options = parseJSON(value);
|
const options = parseJSON(value)
|
||||||
if (!options) {
|
if (!options) {
|
||||||
showAlert("Error!", {
|
showAlert('Error!', {
|
||||||
body: "Cannot save editor with malformed transaction."
|
body: 'Cannot save editor with malformed transaction.'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
};
|
}
|
||||||
|
|
||||||
const { Account, TransactionType, Destination, ...rest } = options;
|
const { Account, TransactionType, Destination, HookParameters, Memos, ...rest } = options
|
||||||
let tx: Partial<TransactionState> = {};
|
let tx: Partial<TransactionState> = {}
|
||||||
const txFields = getTxFields(transactionType)
|
const schema = getTxFields(transactionType)
|
||||||
|
|
||||||
if (Account) {
|
if (Account) {
|
||||||
const acc = state.accounts.find(acc => acc.address === Account);
|
const acc = state.accounts.find(acc => acc.address === Account)
|
||||||
if (acc) {
|
if (acc) {
|
||||||
tx.selectedAccount = {
|
tx.selectedAccount = {
|
||||||
label: acc.name,
|
label: acc.name,
|
||||||
value: acc.address,
|
value: acc.address
|
||||||
};
|
}
|
||||||
} else {
|
|
||||||
tx.selectedAccount = {
|
|
||||||
label: Account,
|
|
||||||
value: Account,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
tx.selectedAccount = null;
|
tx.selectedAccount = {
|
||||||
|
label: Account,
|
||||||
|
value: Account
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
tx.selectedAccount = null
|
||||||
|
}
|
||||||
|
|
||||||
if (TransactionType) {
|
if (TransactionType) {
|
||||||
tx.selectedTransaction = {
|
tx.selectedTransaction = {
|
||||||
label: TransactionType,
|
label: TransactionType,
|
||||||
value: TransactionType,
|
value: TransactionType
|
||||||
};
|
}
|
||||||
|
} else {
|
||||||
|
tx.selectedTransaction = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HookParameters && HookParameters instanceof Array) {
|
||||||
|
tx.hookParameters = HookParameters.reduce<TransactionState["hookParameters"]>((acc, cur, idx) => {
|
||||||
|
const param = { label: fromHex(cur.HookParameter?.HookParameterName || ""), value: fromHex(cur.HookParameter?.HookParameterValue || "") }
|
||||||
|
acc[idx] = param;
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Memos && Memos instanceof Array) {
|
||||||
|
tx.memos = Memos.reduce<TransactionState["memos"]>((acc, cur, idx) => {
|
||||||
|
const memo = { data: fromHex(cur.Memo?.MemoData || ""), type: fromHex(cur.Memo?.MemoType || ""), format: fromHex(cur.Memo?.MemoFormat || "") }
|
||||||
|
acc[idx] = memo;
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.Destination !== undefined) {
|
||||||
|
const dest = state.accounts.find(acc => acc.address === Destination)
|
||||||
|
if (dest) {
|
||||||
|
tx.selectedDestAccount = {
|
||||||
|
label: dest.name,
|
||||||
|
value: dest.address
|
||||||
|
}
|
||||||
|
} else if (Destination) {
|
||||||
|
tx.selectedDestAccount = {
|
||||||
|
label: Destination,
|
||||||
|
value: Destination
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tx.selectedTransaction = null;
|
tx.selectedDestAccount = null
|
||||||
}
|
}
|
||||||
|
} else if (Destination) {
|
||||||
|
rest.Destination = Destination
|
||||||
|
}
|
||||||
|
|
||||||
if (txFields.Destination !== undefined) {
|
if (getFlags(TransactionType) && rest.Flags) {
|
||||||
const dest = state.accounts.find(acc => acc.address === Destination);
|
const flags = extractFlags(TransactionType, rest.Flags)
|
||||||
rest.Destination = null
|
|
||||||
if (dest) {
|
rest.Flags = undefined
|
||||||
tx.selectedDestAccount = {
|
tx.selectedFlags = flags
|
||||||
label: dest.name,
|
}
|
||||||
value: dest.address,
|
|
||||||
};
|
Object.keys(rest).forEach(field => {
|
||||||
}
|
const value = rest[field]
|
||||||
else if (Destination) {
|
const schemaVal = schema[field as keyof TxFields]
|
||||||
tx.selectedDestAccount = {
|
const isXrp =
|
||||||
label: Destination,
|
typeof value !== 'object' &&
|
||||||
value: Destination,
|
schemaVal &&
|
||||||
};
|
typeof schemaVal === 'object' &&
|
||||||
}
|
schemaVal.$type === 'xrp'
|
||||||
else {
|
if (isXrp) {
|
||||||
tx.selectedDestAccount = null
|
rest[field] = {
|
||||||
}
|
$type: 'xrp',
|
||||||
|
$value: +value / 1000000 // ! maybe use bigint?
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
rest[field] = {
|
||||||
|
$type: 'json',
|
||||||
|
$value: value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
Object.keys(rest).forEach(field => {
|
tx.txFields = rest
|
||||||
const value = rest[field];
|
tx.editorIsSaved = true;
|
||||||
const origValue = txFields[field as keyof TxFields]
|
|
||||||
const isXrp = typeof value !== 'object' && origValue && typeof origValue === 'object' && origValue.$type === 'xrp'
|
|
||||||
if (isXrp) {
|
|
||||||
rest[field] = {
|
|
||||||
$type: "xrp",
|
|
||||||
$value: +value / 1000000, // ! maybe use bigint?
|
|
||||||
};
|
|
||||||
} else if (typeof value === "object") {
|
|
||||||
rest[field] = {
|
|
||||||
$type: "json",
|
|
||||||
$value: value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tx.txFields = rest;
|
return tx
|
||||||
tx.editorSavedValue = null;
|
|
||||||
|
|
||||||
return tx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTxFields = (tt?: string) => {
|
export const getTxFields = (tt?: string) => {
|
||||||
const txFields: TxFields | undefined = transactionsData.find(
|
const txFields: TxFields | undefined = transactionsData.find(tx => tx.TransactionType === tt)
|
||||||
tx => tx.TransactionType === tt
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!txFields) return {}
|
if (!txFields) return {}
|
||||||
|
|
||||||
let _txFields = Object.keys(txFields)
|
let _txFields = Object.keys(txFields)
|
||||||
.filter(
|
.filter(key => !commonFields.includes(key as any))
|
||||||
key => !["TransactionType", "Account", "Sequence"].includes(key)
|
.reduce<TxFields>((tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf), {})
|
||||||
)
|
return _txFields
|
||||||
.reduce<TxFields>(
|
|
||||||
(tf, key) => (
|
|
||||||
(tf[key as keyof TxFields] = (txFields as any)[key]), tf
|
|
||||||
),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
return _txFields
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { transactionsData }
|
export { transactionsData, commonFields }
|
||||||
|
|
||||||
|
export const transactionsOptions = transactionsData.map(tx => ({
|
||||||
|
value: tx.TransactionType,
|
||||||
|
label: tx.TransactionType
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const defaultTransactionType = transactionsOptions.find(tt => tt.value === 'Payment')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user