Compare commits

..

1 Commits

Author SHA1 Message Date
muzam1l
aae9c7468f Properly reset 'Destination' field while changing transaction type. 2022-08-03 16:44:27 +05:30
111 changed files with 6028 additions and 6297 deletions

View File

@@ -1,38 +1 @@
See https://help.github.com/articles/ignoring-files/ for more about ignoring files. *.md
# 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

View File

@@ -1,8 +0,0 @@
{
"tabWidth": 2,
"arrowParens": "avoid",
"semi": false,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "none"
}

View File

@@ -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 { capitalize } from "../utils/helpers";
import { deleteAccount } from '../state/actions/deleteAccount'
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(account => account.address === activeAccountAddress) const activeAccount = snap.accounts.find(
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,25 +96,28 @@ 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={() => {
deleteAccount(activeAccount?.address) const index = state.accounts.findIndex(
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' } } a: { "&:hover": { textDecoration: "underline" } },
}} }}
> >
<a <a
@@ -125,94 +129,94 @@ export const AccountDialog = ({
</a> </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
@@ -221,7 +225,7 @@ export const AccountDialog = ({
</Flex> </Flex>
<Flex <Flex
css={{ css={{
marginLeft: 'auto' marginLeft: "auto",
}} }}
> >
<a <a
@@ -229,19 +233,23 @@ export const AccountDialog = ({
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<Button size="sm" ghost css={{ color: '$grass11 !important', mt: '$3' }}> <Button
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' } } a: { "&:hover": { textDecoration: "underline" } },
}} }}
> >
{activeAccount && activeAccount.hooks.length > 0 {activeAccount && activeAccount.hooks.length > 0
@@ -255,20 +263,20 @@ export const AccountDialog = ({
> >
{truncate(i, 12)} {truncate(i, 12)}
</a> </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" />
@@ -279,109 +287,117 @@ 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 { interface AccountProps {
card?: boolean card?: boolean;
hideDeployBtn?: boolean hideDeployBtn?: boolean;
showHookStats?: boolean showHookStats?: boolean;
} }
const Accounts: FC<AccountProps> = props => { const Accounts: FC<AccountProps> = props => {
const snap = useSnapshot(state) const snap = useSnapshot(state);
const [activeAccountAddress, setActiveAccountAddress] = useState<string | null>(null) 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({ snap.client?.send({
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(acc => acc.address === address) const accountToUpdate = state.accounts.find(
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(acc => acc.address === res?.account) const oldAccount = state.accounts.find(
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 snap.client?.send({
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(acc => acc.address === address) const accountToUpdate = state.accounts.find(
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
@@ -389,33 +405,33 @@ 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" }}>
<ImportAccountDialog type="create" /> <ImportAccountDialog type="create" />
<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 => (
@@ -424,45 +440,45 @@ const Accounts: FC<AccountProps> = props => {
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>
)} )}
@@ -472,7 +488,7 @@ const Accounts: FC<AccountProps> = props => {
<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} />
@@ -480,9 +496,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>
@@ -494,33 +510,37 @@ 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 = ({ type = 'import' }: { type?: 'import' | 'create' }) => { const ImportAccountDialog = ({
const [secret, setSecret] = useState('') type = "import",
const [name, setName] = useState('') }: {
type?: "import" | "create";
}) => {
const [secret, setSecret] = useState("");
const [name, setName] = useState("");
const btnText = type === 'import' ? 'Import' : 'Create' const btnText = type === "import" ? "Import" : "Create";
const title = type === 'import' ? 'Import Account' : 'Create Account' const title = type === "import" ? "Import Account" : "Create Account";
const handleSubmit = async () => { const handleSubmit = async () => {
if (type === 'create') { if (type === "create") {
const value = capitalize(name) const value = capitalize(name);
await addFaucetAccount(value, true) await addFaucetAccount(value, true);
setName('') setName("");
setSecret('') setSecret("");
return return;
} }
importAccount(secret, name) importAccount(secret, name);
setName('') setName("");
setSecret('') setSecret("");
} };
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -529,9 +549,9 @@ const ImportAccountDialog = ({ type = 'import' }: { type?: 'import' | 'create' }
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent aria-describedby={undefined}> <DialogContent aria-describedby={undefined}>
<DialogTitle css={{ mb: '$4' }}>{title}</DialogTitle> <DialogTitle css={{ mb: "$4" }}>{title}</DialogTitle>
<Flex column> <Flex column>
<Box css={{ mb: '$2' }}> <Box css={{ mb: "$2" }}>
<Label> <Label>
Account name <Text muted>(optional)</Text> Account name <Text muted>(optional)</Text>
</Label> </Label>
@@ -544,7 +564,7 @@ const ImportAccountDialog = ({ type = 'import' }: { type?: 'import' | 'create' }
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
/> />
</Box> </Box>
{type === 'import' && ( {type === "import" && (
<Box> <Box>
<Label>Account secret</Label> <Label>Account secret</Label>
<Input <Input
@@ -562,8 +582,8 @@ const ImportAccountDialog = ({ type = 'import' }: { type?: 'import' | 'create' }
<Flex <Flex
css={{ css={{
marginTop: 25, marginTop: 25,
justifyContent: 'flex-end', justifyContent: "flex-end",
gap: '$3' gap: "$3",
}} }}
> >
<DialogClose asChild> <DialogClose asChild>
@@ -576,13 +596,13 @@ const ImportAccountDialog = ({ type = 'import' }: { type?: 'import' | 'create' }
</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;

View File

@@ -1,62 +1,65 @@
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 open={isOpen} onOpenChange={value => (alertState.isOpen = value)}> <AlertDialog
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}
@@ -66,7 +69,7 @@ const Alert: FC = () => {
</Flex> </Flex>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
) );
} };
export default Alert export default Alert;

View File

@@ -1,83 +1,88 @@
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> = ({ children, ...rest }) => { const Root: React.FC<AlertDialogPrimitive.AlertDialogProps> = ({
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: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)', boxShadow:
position: 'fixed', "0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
top: '50%', position: "fixed",
left: '50%', top: "50%",
transform: 'translate(-50%, -50%)', left: "50%",
width: '90vw', transform: "translate(-50%, -50%)",
maxWidth: '450px', width: "90vw",
maxHeight: '85vh', maxWidth: "450px",
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: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)' boxShadow:
} "0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
}) },
});
const StyledTitle = styled(AlertDialogPrimitive.Title, { 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;

View File

@@ -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;

View File

@@ -1,285 +1,291 @@
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: 'inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12' boxShadow:
"inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12",
}, },
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]': '&[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<React.ComponentProps<typeof StyledButton> & { as?: string }> = const CustomButton: React.FC<
React.forwardRef(({ children, as = 'button', ...rest }, ref) => ( React.ComponentProps<typeof StyledButton> & { as?: string }
// @ts-expect-error > = React.forwardRef(({ children, as = "button", ...rest }, ref) => (
<StyledButton {...rest} ref={ref} as={as}> // @ts-expect-error
<Flex as="span" css={{ gap: '$2', alignItems: 'center' }} className="button-content"> <StyledButton {...rest} ref={ref} as={as}>
{children} <Flex
</Flex> as="span"
{rest.isLoading && <Spinner css={{ position: 'absolute' }} />} css={{ gap: "$2", alignItems: "center" }}
</StyledButton> className="button-content"
)) >
{children}
</Flex>
{rest.isLoading && <Spinner css={{ position: "absolute" }} />}
</StyledButton>
));
CustomButton.displayName = 'CustomButton' CustomButton.displayName = "CustomButton";
export default CustomButton export default CustomButton;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
import { CaretRight, Check, Circle } from 'phosphor-react' import { CaretRight, Check, Circle } from "phosphor-react";
import { FC, Fragment, ReactNode } from 'react' import { FC, Fragment, ReactNode } from "react";
import { Flex, Text } from '../' import { Flex, Text } from "../";
import { import {
ContextMenuCheckboxItem, ContextMenuCheckboxItem,
ContextMenuContent, ContextMenuContent,
@@ -12,40 +12,49 @@ import {
ContextMenuRoot, ContextMenuRoot,
ContextMenuSeparator, ContextMenuSeparator,
ContextMenuTrigger, ContextMenuTrigger,
ContextMenuTriggerItem ContextMenuTriggerItem,
} from './primitive' } from "./primitive";
export type TextOption = { export type TextOption = {
type: 'text' type: "text";
label: ReactNode label: ReactNode;
onSelect?: () => any onSelect?: () => any;
children?: ContentMenuOption[] children?: ContentMenuOption[];
} };
export type SeparatorOption = { type: 'separator' } export type SeparatorOption = { type: "separator" };
export type CheckboxOption = { export type CheckboxOption = {
type: 'checkbox' type: "checkbox";
label: ReactNode label: ReactNode;
checked?: boolean checked?: boolean;
onCheckedChange?: (isChecked: boolean) => any onCheckedChange?: (isChecked: boolean) => any;
} };
export type RadioOption<T extends string = string> = { export type RadioOption<T extends string = string> = {
type: 'radio' type: "radio";
label: ReactNode label: ReactNode;
onValueChange?: (value: string) => any onValueChange?: (value: string) => any;
value: T value: T;
options?: { value: T; label?: ReactNode }[] options?: { value: T; label?: ReactNode }[];
} };
type WithCommons = { key: string; disabled?: boolean } type WithCommons = { key: string; disabled?: boolean };
export type ContentMenuOption = (TextOption | SeparatorOption | CheckboxOption | RadioOption) & export type ContentMenuOption = (
WithCommons | TextOption
| SeparatorOption
| CheckboxOption
| RadioOption
) &
WithCommons;
export interface IContextMenu { export interface IContextMenu {
options?: ContentMenuOption[] options?: ContentMenuOption[];
isNested?: boolean isNested?: boolean;
} }
export const ContextMenu: FC<IContextMenu> = ({ children, options, isNested }) => { export const ContextMenu: FC<IContextMenu> = ({
children,
options,
isNested,
}) => {
return ( return (
<ContextMenuRoot> <ContextMenuRoot>
{isNested ? ( {isNested ? (
@@ -56,8 +65,8 @@ export const ContextMenu: FC<IContextMenu> = ({ children, options, isNested }) =
{options && !!options.length && ( {options && !!options.length && (
<ContextMenuContent sideOffset={isNested ? 2 : 5}> <ContextMenuContent sideOffset={isNested ? 2 : 5}>
{options.map(({ key, ...option }) => { {options.map(({ key, ...option }) => {
if (option.type === 'text') { if (option.type === "text") {
const { children, label, onSelect } = option const { children, label, onSelect } = option;
if (children) if (children)
return ( return (
<ContextMenu isNested key={key} options={children}> <ContextMenu isNested key={key} options={children}>
@@ -66,15 +75,15 @@ export const ContextMenu: FC<IContextMenu> = ({ children, options, isNested }) =
<CaretRight /> <CaretRight />
</Flex> </Flex>
</ContextMenu> </ContextMenu>
) );
return ( return (
<ContextMenuItem key={key} onSelect={onSelect}> <ContextMenuItem key={key} onSelect={onSelect}>
{label} {label}
</ContextMenuItem> </ContextMenuItem>
) );
} }
if (option.type === 'checkbox') { if (option.type === "checkbox") {
const { label, checked, onCheckedChange } = option const { label, checked, onCheckedChange } = option;
return ( return (
<ContextMenuCheckboxItem <ContextMenuCheckboxItem
key={key} key={key}
@@ -85,37 +94,42 @@ export const ContextMenu: FC<IContextMenu> = ({ children, options, isNested }) =
<ContextMenuItemIndicator> <ContextMenuItemIndicator>
<Check /> <Check />
</ContextMenuItemIndicator> </ContextMenuItemIndicator>
<Text css={{ ml: checked ? '$4' : undefined }}>{label}</Text> <Text css={{ ml: checked ? "$4" : undefined }}>
{label}
</Text>
</Flex> </Flex>
</ContextMenuCheckboxItem> </ContextMenuCheckboxItem>
) );
} }
if (option.type === 'radio') { if (option.type === "radio") {
const { label, options, onValueChange, value } = option const { label, options, onValueChange, value } = option;
return ( return (
<Fragment key={key}> <Fragment key={key}>
<ContextMenuLabel>{label}</ContextMenuLabel> <ContextMenuLabel>{label}</ContextMenuLabel>
<ContextMenuRadioGroup value={value} onValueChange={onValueChange}> <ContextMenuRadioGroup
value={value}
onValueChange={onValueChange}
>
{options?.map(({ value: v, label }) => { {options?.map(({ value: v, label }) => {
return ( return (
<ContextMenuRadioItem key={v} value={v}> <ContextMenuRadioItem key={v} value={v}>
<ContextMenuItemIndicator> <ContextMenuItemIndicator>
<Circle weight="fill" /> <Circle weight="fill" />
</ContextMenuItemIndicator> </ContextMenuItemIndicator>
<Text css={{ ml: '$4' }}>{label}</Text> <Text css={{ ml: "$4" }}>{label}</Text>
</ContextMenuRadioItem> </ContextMenuRadioItem>
) );
})} })}
</ContextMenuRadioGroup> </ContextMenuRadioGroup>
</Fragment> </Fragment>
) );
} }
return <ContextMenuSeparator key={key} /> return <ContextMenuSeparator key={key} />;
})} })}
</ContextMenuContent> </ContextMenuContent>
)} )}
</ContextMenuRoot> </ContextMenuRoot>
) );
} };
export default ContextMenu export default ContextMenu;

View File

@@ -1,107 +1,107 @@
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu' import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { styled } from '../../stitches.config' import { styled } from "../../stitches.config";
import { import {
slideDownAndFade, slideDownAndFade,
slideLeftAndFade, slideLeftAndFade,
slideRightAndFade, slideRightAndFade,
slideUpAndFade slideUpAndFade,
} from '../../styles/keyframes' } from "../../styles/keyframes";
const StyledContent = styled(ContextMenuPrimitive.Content, { const StyledContent = styled(ContextMenuPrimitive.Content, {
minWidth: 140, minWidth: 140,
backgroundColor: '$backgroundOverlay', backgroundColor: "$backgroundOverlay",
borderRadius: 6, borderRadius: 6,
overflow: 'hidden', overflow: "hidden",
padding: '5px', padding: "5px",
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 &": {
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: '$text', color: "$text",
borderRadius: 3, borderRadius: 3,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
height: 28, height: 28,
padding: '0 7px', padding: "0 7px",
position: 'relative', position: "relative",
paddingLeft: 10, paddingLeft: 10,
userSelect: 'none', userSelect: "none",
'&[data-disabled]': { "&[data-disabled]": {
color: '$textMuted', color: "$textMuted",
pointerEvents: 'none' pointerEvents: "none",
}, },
'&:focus': { "&:focus": {
backgroundColor: '$purple9', backgroundColor: "$purple9",
color: '$white' color: "$white",
} },
} };
const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles }) const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles });
const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, { const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, {
...itemStyles ...itemStyles,
}) });
const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, { const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, {
...itemStyles ...itemStyles,
}) });
const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, { const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, {
'&[data-state="open"]': { '&[data-state="open"]': {
backgroundColor: '$purple9', backgroundColor: "$purple9",
color: '$purple9' color: "$purple9",
}, },
...itemStyles ...itemStyles,
}) });
const StyledLabel = styled(ContextMenuPrimitive.Label, { const StyledLabel = styled(ContextMenuPrimitive.Label, {
paddingLeft: 10, paddingLeft: 10,
fontSize: 12, fontSize: 12,
lineHeight: '25px', lineHeight: "25px",
color: '$text' color: "$text",
}) });
const StyledSeparator = styled(ContextMenuPrimitive.Separator, { const StyledSeparator = styled(ContextMenuPrimitive.Separator, {
height: 1, height: 1,
backgroundColor: '$backgroundAlt', backgroundColor: "$backgroundAlt",
margin: 5 margin: 5,
}) });
const StyledItemIndicator = styled(ContextMenuPrimitive.ItemIndicator, { const StyledItemIndicator = styled(ContextMenuPrimitive.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",
}) });
export const ContextMenuRoot = ContextMenuPrimitive.Root export const ContextMenuRoot = ContextMenuPrimitive.Root;
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
export const ContextMenuContent = StyledContent export const ContextMenuContent = StyledContent;
export const ContextMenuItem = StyledItem export const ContextMenuItem = StyledItem;
export const ContextMenuCheckboxItem = StyledCheckboxItem export const ContextMenuCheckboxItem = StyledCheckboxItem;
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
export const ContextMenuRadioItem = StyledRadioItem export const ContextMenuRadioItem = StyledRadioItem;
export const ContextMenuItemIndicator = StyledItemIndicator export const ContextMenuItemIndicator = StyledItemIndicator;
export const ContextMenuTriggerItem = StyledTriggerItem export const ContextMenuTriggerItem = StyledTriggerItem;
export const ContextMenuLabel = StyledLabel export const ContextMenuLabel = StyledLabel;
export const ContextMenuSeparator = StyledSeparator export const ContextMenuSeparator = StyledSeparator;

View File

@@ -1,113 +1,113 @@
import { useEffect } from 'react' import { useEffect } from "react";
import ReconnectingWebSocket, { CloseEvent } from 'reconnecting-websocket' import ReconnectingWebSocket, { CloseEvent } from "reconnecting-websocket";
import { proxy, ref, useSnapshot } from 'valtio' import { proxy, ref, useSnapshot } from "valtio";
import { subscribeKey } from 'valtio/utils' import { subscribeKey } from "valtio/utils";
import { Select } from '.' import { Select } from ".";
import state, { ILog, transactionsState } from '../state' import state, { ILog, transactionsState } from "../state";
import { extractJSON } from '../utils/json' import { extractJSON } from "../utils/json";
import LogBox from './LogBox' 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?: ReconnectingWebSocket 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) => { const onOpen = (account: ISelect | null) => {
if (!account) { if (!account) {
return return;
} }
// streamState.logs = []; // streamState.logs = [];
streamState.status = 'opened' streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now() streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account?.value}`, { pushLog(`Debug stream opened for account ${account?.value}`, {
type: 'success' type: "success",
}) });
} };
const onError = () => { const onError = () => {
pushLog('Something went wrong! Check your connection and try again.', { pushLog("Something went wrong! Check your connection and try again.", {
type: 'error' type: "error",
}) });
} };
const onClose = (e: CloseEvent) => { const onClose = (e: CloseEvent) => {
// 999 = closed websocket connection by switching account // 999 = closed websocket connection by switching account
if (e.code !== 4999) { if (e.code !== 4999) {
pushLog(`Connection was closed. [code: ${e.code}]`, { pushLog(`Connection was closed. [code: ${e.code}]`, {
type: 'error' type: "error",
}) });
} }
streamState.status = 'closed' streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now() streamState.statusChangeTimestamp = Date.now();
} };
const onMessage = (event: any) => { const onMessage = (event: any) => {
// Ping returns just account address, if we get that // Ping returns just account address, if we get that
// response we don't need to log anything // response we don't need to log anything
if (event.data !== streamState.selectedAccount?.value) { if (event.data !== streamState.selectedAccount?.value) {
pushLog(event.data) pushLog(event.data);
} }
} };
let interval: NodeJS.Timer | null = null let interval: NodeJS.Timer | null = null;
const addListeners = (account: ISelect | null) => { const addListeners = (account: ISelect | null) => {
if (account?.value && streamState.socket?.url.endsWith(account?.value)) { if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
return return;
} }
streamState.logs = [] streamState.logs = [];
if (account?.value) { if (account?.value) {
if (interval) { if (interval) {
clearInterval(interval) clearInterval(interval);
} }
if (streamState.socket) { if (streamState.socket) {
streamState.socket?.removeEventListener('open', () => onOpen(account)) streamState.socket?.removeEventListener("open", () => onOpen(account));
streamState.socket?.removeEventListener('close', onClose) streamState.socket?.removeEventListener("close", onClose);
streamState.socket?.removeEventListener('error', onError) streamState.socket?.removeEventListener("error", onError);
streamState.socket?.removeEventListener('message', onMessage) streamState.socket?.removeEventListener("message", onMessage);
} }
streamState.socket = ref( streamState.socket = ref(
new ReconnectingWebSocket( new ReconnectingWebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}` `wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
) )
) );
if (streamState.socket) { if (streamState.socket) {
interval = setInterval(() => { interval = setInterval(() => {
streamState.socket?.send('') streamState.socket?.send("");
}, 45000) }, 45000);
} }
streamState.socket.addEventListener('open', () => onOpen(account)) streamState.socket.addEventListener("open", () => onOpen(account));
streamState.socket.addEventListener('close', onClose) streamState.socket.addEventListener("close", onClose);
streamState.socket.addEventListener('error', onError) streamState.socket.addEventListener("error", onError);
streamState.socket.addEventListener('message', onMessage) streamState.socket.addEventListener("message", onMessage);
} }
} };
subscribeKey(streamState, 'selectedAccount', addListeners) subscribeKey(streamState, "selectedAccount", addListeners);
const DebugStream = () => { const DebugStream = () => {
const { selectedAccount, logs } = 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 = () => (
<> <>
@@ -117,63 +117,80 @@ const DebugStream = () => {
options={accountOptions} options={accountOptions}
hideSelectedOptions hideSelectedOptions
value={selectedAccount} value={selectedAccount}
onChange={acc => { onChange={(acc) => {
streamState.socket?.close(4999, 'Old connection closed because user switched account') streamState.socket?.close(
streamState.selectedAccount = acc as any 4999,
"Old connection closed because user switched account"
);
streamState.selectedAccount = acc as any;
}} }}
css={{ width: '100%' }} css={{ width: "100%" }}
/> />
</> </>
) );
useEffect(() => { useEffect(() => {
const account = transactionsState.transactions.find(tx => tx.header === activeTxTab)?.state const account = transactionsState.transactions.find(
.selectedAccount (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 = () => { const clearLog = () => {
streamState.logs = [] streamState.logs = [];
streamState.statusChangeTimestamp = Date.now() streamState.statusChangeTimestamp = Date.now();
} };
return ( return (
<LogBox enhanced renderNav={renderNav} title="Debug stream" logs={logs} clearLog={clearLog} /> <LogBox
) enhanced
} renderNav={renderNav}
title="Debug stream"
logs={logs}
clearLog={clearLog}
/>
);
};
export default DebugStream export default DebugStream;
export const pushLog = (str: any, opts: Partial<Pick<ILog, 'type'>> = {}): ILog | undefined => { export const pushLog = (
if (!str) return str: any,
if (typeof str !== 'string') throw Error('Unrecognized debug log stream!') opts: Partial<Pick<ILog, "type">> = {}
): ILog | undefined => {
if (!str) return;
if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m) const 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 ? msg : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1) const message = !extracted
? msg
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
const jsonData = extracted ? JSON.stringify(extracted.result, null, 2) : undefined const jsonData = extracted
? JSON.stringify(extracted.result, null, 2)
: 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;
} };

View File

@@ -1,50 +1,54 @@
import React, { useState } from 'react' import React, { useState } from "react";
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio";
import { useTheme } from 'next-themes' import { useTheme } from "next-themes";
import { useRouter } from 'next/router' import { useRouter } from "next/router";
import NextLink from 'next/link' import NextLink from "next/link";
import ReactTimeAgo from 'react-time-ago' import ReactTimeAgo from "react-time-ago";
import filesize from 'filesize' import filesize from "filesize";
import Box from './Box' import Box from "./Box";
import Container from './Container' import Container from "./Container";
import state from '../state' import state from "../state";
import wat from '../utils/wat-highlight' import wat from "../utils/wat-highlight";
import EditorNavigation from './EditorNavigation' import EditorNavigation from "./EditorNavigation";
import { Button, Text, Link, Flex, Tabs, Tab } from '.' import { Button, Text, Link, Flex, Tabs, Tab } from ".";
import Monaco from './Monaco' 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 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 compiledFiles = snap.files.filter(file => file.compiledContent) const compiledFiles = snap.files.filter(file => file.compiledContent);
const activeFile = compiledFiles[snap.activeWat] const activeFile = compiledFiles[snap.activeWat];
const renderNav = () => ( const renderNav = () => (
<Tabs activeIndex={snap.activeWat} onChangeActive={idx => (state.activeWat = idx)}> <Tabs
activeIndex={snap.activeWat}
onChangeActive={idx => (state.activeWat = idx)}
>
{compiledFiles.map((file, index) => { {compiledFiles.map((file, index) => {
return <Tab key={file.name} header={`${file.name}.wat`} /> return <Tab key={file.name} header={`${file.name}.wat`} />;
})} })}
</Tabs> </Tabs>
) );
const compiledSize = activeFile?.compiledContent?.byteLength || 0 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 isContentChanged =
activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
// const hasDeployErrors = activeFile && activeFile.containsErrors; // const hasDeployErrors = activeFile && activeFile.containsErrors;
const CompiledStatView = activeFile && ( const CompiledStatView = activeFile && (
@@ -52,45 +56,52 @@ const DeployEditor = () => {
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' }}>Compiled {activeFile.name.split('.')[0] + '.wasm'}</Text> <Text css={{ mr: "$1" }}>
{activeFile?.lastCompiled && <ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />} Compiled {activeFile.name.split(".")[0] + ".wasm"}
</Text>
{activeFile?.lastCompiled && (
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
)}
{activeFile.compiledContent?.byteLength && ( {activeFile.compiledContent?.byteLength && (
<Text css={{ ml: '$2', color }}>({filesize(activeFile.compiledContent.byteLength)})</Text> <Text css={{ ml: "$2", color }}>
({filesize(activeFile.compiledContent.byteLength)})
</Text>
)} )}
</Flex> </Flex>
{activeFile.compiledContent?.byteLength && activeFile.compiledContent?.byteLength >= 64000 && ( {activeFile.compiledContent?.byteLength &&
<Flex css={{ flexDirection: 'column', py: '$3', pb: '$1' }}> activeFile.compiledContent?.byteLength >= 64000 && (
<Text css={{ ml: '$2', color: '$error' }}> <Flex css={{ flexDirection: "column", py: "$3", pb: "$1" }}>
File size is larger than 64kB, cannot set hook! <Text css={{ ml: "$2", color: "$error" }}>
</Text> File size is larger than 64kB, cannot set hook!
</Flex> </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 && ( {isContentChanged && (
<Text warning> <Text warning>
File contents were changed after last compile, compile again to incorporate your latest File contents were changed after last compile, compile again to
changes in the build. incorporate your latest changes in the build.
</Text> </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 `}
@@ -98,27 +109,29 @@ 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 && router.isReady const isContent =
snap.files?.filter(file => file.compiledWatContent).length > 0 &&
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 renderNav={renderNav} /> <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 ? (
@@ -128,39 +141,41 @@ const DeployEditor = () => {
) : ( ) : (
<Monaco <Monaco
className="hooks-editor" className="hooks-editor"
defaultLanguage={'wat'} defaultLanguage={"wat"}
language={'wat'} language={"wat"}
path={`file://tmp/c/${activeFile?.name}.wat`} path={`file://tmp/c/${activeFile?.name}.wat`}
value={activeFile?.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);
}} }}
onMount={editor => { onMount={editor => {
editor.updateOptions({ editor.updateOptions({
glyphMargin: true, glyphMargin: true,
readOnly: true readOnly: true,
}) });
}} }}
theme={theme === 'dark' ? 'dark' : 'light'} theme={theme === "dark" ? "dark" : "light"}
overlay={ overlay={
<Flex <Flex
css={{ css={{
m: '$1', m: "$1",
ml: 'auto', ml: "auto",
fontSize: '$sm', fontSize: "$sm",
color: '$textMuted' color: "$textMuted",
}} }}
> >
<Link onClick={() => setShowContent(false)}>Exit editor mode</Link> <Link onClick={() => setShowContent(false)}>
Exit editor mode
</Link>
</Flex> </Flex>
} }
/> />
)} )}
</Container> </Container>
</Box> </Box>
) );
} };
export default DeployEditor export default DeployEditor;

View File

@@ -1,88 +1,90 @@
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: 10000, zIndex: 10000,
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: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)', boxShadow:
width: '90vw', "0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
maxWidth: '450px', width: "90vw",
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: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)' boxShadow:
} "0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
}) },
});
const Content: React.FC<{ css?: Stiches.CSS }> = ({ css, children }) => { 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;

View File

@@ -1,120 +1,115 @@
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 { import { slideDownAndFade, slideLeftAndFade, slideRightAndFade, slideUpAndFade } from '../styles/keyframes';
slideDownAndFade,
slideLeftAndFade,
slideRightAndFade,
slideUpAndFade
} from '../styles/keyframes'
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;

View File

@@ -1,4 +1,9 @@
import React, { useState, useEffect, useRef, ReactNode } from 'react' import React, {
useState,
useEffect,
useRef,
ReactNode,
} from "react";
import { import {
Share, Share,
DownloadSimple, DownloadSimple,
@@ -10,113 +15,120 @@ 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 { syncToGist, updateEditorSettings, downloadAsZip } from '../state/actions' import {
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 Tooltip from './Tooltip' import Tooltip from "./Tooltip";
import { showAlert } from '../state/actions/showAlert' import { showAlert } from "../state/actions/showAlert";
const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => { const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
const snap = useSnapshot(state) const snap = useSnapshot(state);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false) const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const { data: session, status } = useSession() const { data: session, status } = useSession();
const [popup, setPopUp] = useState(false) const [popup, setPopUp] = useState(false);
const [editorSettings, setEditorSettings] = useState(snap.editorSettings) 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]);
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 your current saved This action will create new <strong>public</strong> Github Gist from
files. You can delete gist anytime from your GitHub Gists page. your current saved files. You can delete gist anytime from your GitHub
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 scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null);
return ( return (
<Flex css={{ flexShrink: 0, gap: '$0' }}> <Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex <Flex
id="kissa" id="kissa"
ref={scrollRef} ref={scrollRef}
css={{ css={{
overflowX: 'scroll', overflowX: "scroll",
overflowY: 'hidden', overflowY: "hidden",
py: '$3', py: "$3",
pb: '$0', pb: "$0",
flex: 1, flex: 1,
'&::-webkit-scrollbar': { "&::-webkit-scrollbar": {
height: '0.3em', height: "0.3em",
background: 'rgba(0,0,0,.0)' background: "rgba(0,0,0,.0)",
}, },
'&::-webkit-scrollbar-gutter': 'stable', "&::-webkit-scrollbar-gutter": "stable",
'&::-webkit-scrollbar-thumb': { "&::-webkit-scrollbar-thumb": {
backgroundColor: 'rgba(0,0,0,.2)', backgroundColor: "rgba(0,0,0,.2)",
outline: '0px', outline: "0px",
borderRadius: '9999px' borderRadius: "9999px",
}, },
scrollbarColor: 'rgba(0,0,0,.2) rgba(0,0,0,0)', scrollbarColor: "rgba(0,0,0,.2) rgba(0,0,0,0)",
scrollbarGutter: 'stable', scrollbarGutter: "stable",
scrollbarWidth: 'thin', scrollbarWidth: "thin",
'.dark &': { ".dark &": {
'&::-webkit-scrollbar': { "&::-webkit-scrollbar": {
background: 'rgba(0,0,0,.0)' background: "rgba(0,0,0,.0)",
}, },
'&::-webkit-scrollbar-gutter': 'stable', "&::-webkit-scrollbar-gutter": "stable",
'&::-webkit-scrollbar-thumb': { "&::-webkit-scrollbar-thumb": {
backgroundColor: 'rgba(255,255,255,.2)', backgroundColor: "rgba(255,255,255,.2)",
outline: '0px', outline: "0px",
borderRadius: '9999px' borderRadius: "9999px",
}, },
scrollbarColor: 'rgba(255,255,255,.2) rgba(0,0,0,0)', scrollbarColor: "rgba(255,255,255,.2) rgba(0,0,0,0)",
scrollbarGutter: 'stable', scrollbarGutter: "stable",
scrollbarWidth: 'thin' scrollbarWidth: "thin",
} },
}} }}
onWheelCapture={e => { onWheelCapture={e => {
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY scrollRef.current.scrollLeft += e.deltaY;
} }
}} }}
> >
@@ -126,35 +138,37 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</Flex> </Flex>
<Flex <Flex
css={{ css={{
py: '$3', py: "$3",
backgroundColor: '$mauve2', backgroundColor: "$mauve2",
zIndex: 1 zIndex: 1,
}} }}
> >
<Container css={{ width: 'unset', display: 'flex', alignItems: 'center' }}> <Container
{status === 'authenticated' ? ( css={{ width: "unset", display: "flex", alignItems: "center" }}
>
{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"
@@ -164,16 +178,21 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem disabled onClick={() => signOut()}> <DropdownMenuItem disabled onClick={() => signOut()}>
<User size="16px" /> {session?.user?.username} ({session?.user.name}) <User size="16px" /> {session?.user?.username} (
{session?.user.name})
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => window.open(`http://gist.github.com/${session?.user.username}`)} onClick={() =>
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>
@@ -181,43 +200,48 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) : ( ) : (
<Button outline size="sm" css={{ mr: '$3' }} onClick={() => setPopUp(true)}> <Button
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">
@@ -226,7 +250,7 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
onClick={downloadAsZip} onClick={downloadAsZip}
outline outline
size="sm" size="sm"
css={{ alignItems: 'center' }} css={{ alignItems: "center" }}
> >
<DownloadSimple size="16px" /> <DownloadSimple size="16px" />
</Button> </Button>
@@ -235,10 +259,12 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
<Button <Button
outline outline
size="sm" size="sm"
css={{ alignItems: 'center' }} css={{ alignItems: "center" }}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/develop/${snap.gistId}`) navigator.clipboard.writeText(
toast.success('Copied share link to clipboard!') `${window.location.origin}/develop/${snap.gistId}`
);
toast.success("Copied share link to clipboard!");
}} }}
> >
<Share size="16px" /> <Share size="16px" />
@@ -248,9 +274,9 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
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
@@ -258,15 +284,15 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
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();
} }
}} }}
> >
@@ -285,33 +311,38 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem disabled={snap.zipLoading} onClick={downloadAsZip}> <DropdownMenuItem
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={session?.user.username !== snap.gistOwner || !snap.gistId} disabled={
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
@@ -326,7 +357,9 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</DropdownMenu> </DropdownMenu>
</Stack> </Stack>
{popup && !session ? <NewWindow center="parent" url="/sign-in" /> : null} {popup && !session ? (
<NewWindow center="parent" url="/sign-in" />
) : null}
</Container> </Container>
</Flex> </Flex>
@@ -347,33 +380,39 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
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 outline onClick={() => updateEditorSettings(editorSettings)}> <Button
outline
onClick={() => updateEditorSettings(editorSettings)}
>
Cancel Cancel
</Button> </Button>
</DialogClose> </DialogClose>
<DialogClose asChild> <DialogClose asChild>
<Button variant="primary" onClick={() => updateEditorSettings(editorSettings)}> <Button
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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,46 +1,39 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from 'valtio' import { useSnapshot, ref } from "valtio";
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 asc from 'assemblyscript/dist/asc' import { createNewFile, saveFile } from "../state/actions";
import { createNewFile, saveFile } from '../state/actions' import { apiHeaderFiles } from "../state/constants";
import { apiHeaderFiles } from '../state/constants' import state from "../state";
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 Monaco from "./Monaco";
import { saveAllFiles } from '../state/actions/saveFile' import { saveAllFiles } from "../state/actions/saveFile";
import { Tab, Tabs } from './Tabs' import { Tab, Tabs } from "./Tabs";
import { renameFile } from '../state/actions/createNewFile' 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 filename = editor.getModel()?.uri.path.split('/').pop() const currPath = editor.getModel()?.uri.path;
const isWritable = checkWritable(filename) if (apiHeaderFiles.find(h => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: !isWritable }) editor.updateOptions({ readOnly: true });
} } 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,
@@ -51,10 +44,10 @@ const setMarkers = (monacoE: typeof monaco) => {
// 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(
@@ -63,13 +56,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.split('/').includes(`${state.files?.[state.active]?.name}`) marker?.resource.path
.split("/")
.includes(`${state.files?.[state.active]?.name}`)
) )
.map(marker => ({ .map(marker => ({
range: new monacoE.Range( range: new monacoE.Range(
@@ -85,46 +80,47 @@ 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' && docs[marker?.code]?.toString()) || '', (typeof marker.code === "string" &&
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(() => { useEffect(() => {
return () => { return () => {
saveAllFiles() saveAllFiles();
} };
}, []) }, []);
const file = snap.files[snap.active] const file = snap.files[snap.active];
const renderNav = () => ( const renderNav = () => (
<Tabs <Tabs
@@ -134,190 +130,158 @@ const HooksEditor = () => {
extensionRequired extensionRequired
onCreateNewTab={createNewFile} onCreateNewTab={createNewFile}
onCloseTab={idx => state.files.splice(idx, 1)} onCloseTab={idx => state.files.splice(idx, 1)}
onRenameTab={(idx, nwName, oldName = '') => renameFile(oldName, nwName)} onRenameTab={(idx, nwName, oldName = "") => renameFile(oldName, nwName)}
headerExtraValidation={{ headerExtraValidation={{
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g, 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 "-"' error:
'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"',
}} }}
> >
{snap.files.map((file, index) => { {snap.files.map((file, index) => {
return <Tab key={file.name} header={file.name} renameDisabled={!checkWritable(file.name)} /> return <Tab key={file.name} header={file.name} />;
})} })}
</Tabs> </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 renderNav={renderNav} /> <EditorNavigation renderNav={renderNav} />
{file?.language === 'markdown' && previewToggle}
{snap.files.length > 0 && router.isReady ? ( {snap.files.length > 0 && router.isReady ? (
isMdPreview && file?.language === 'markdown' ? ( <Monaco
<Markdown keepCurrentModel
components={{ defaultLanguage={file?.language}
a: ({ href, children }) => ( language={file?.language}
<Link target="_blank" rel="noopener noreferrer" href={href}> path={`file:///work/c/${file?.name}`}
{children} defaultValue={file?.content}
</Link> // onChange={val => (state.files[snap.active].content = val)} // Auto save?
) beforeMount={monaco => {
}} if (!snap.editorCtx) {
> snap.files.forEach(file =>
{file.content} monaco.editor.createModel(
</Markdown> file.content,
) : ( file.language,
<Monaco monaco.Uri.parse(`file:///work/c/${file.name}`)
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"],
});
MonacoServices.install(monaco);
const webSocket = createWebSocket(
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ""
);
subscriptionRef.current = webSocket;
// listen when the web socket is opened
listen({
webSocket: webSocket as WebSocket,
onConnection: connection => {
// create and start the language client
const languageClient = createLanguageClient(connection);
const disposable = languageClient.start();
connection.onClose(() => {
try {
disposable.dispose();
} catch (err) {
console.log("err", err);
}
});
},
});
}
// editor.updateOptions({
// minimap: {
// enabled: false,
// },
// ...snap.editorSettings,
// });
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
}
}}
onMount={(editor, monaco) => {
editorRef.current = editor;
monacoRef.current = monaco;
editor.updateOptions({
glyphMargin: true,
lightbulb: {
enabled: true,
},
});
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
() => {
saveFile();
} }
);
monaco.languages.typescript.typescriptDefaults.addExtraLib( // When the markers (errors/warnings from clangd language server) change
asc.definitionFiles.assembly, // Lets improve the markers by adding extra content to them from related
'assemblyscript/std/assembly/index.d.ts' // md files
) monaco.editor.onDidChangeMarkers(() => {
if (monacoRef.current) {
// create the web socket setMarkers(monacoRef.current);
if (!subscriptionRef.current) {
monaco.languages.register({
id: 'c',
extensions: ['.c', '.h'],
aliases: ['C', 'c', 'H', 'h'],
mimetypes: ['text/plain']
})
MonacoServices.install(monaco)
const webSocket = createWebSocket(
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ''
)
subscriptionRef.current = webSocket
// listen when the web socket is opened
listen({
webSocket: webSocket as WebSocket,
onConnection: connection => {
// create and start the language client
const languageClient = createLanguageClient(connection)
const disposable = languageClient.start()
connection.onClose(() => {
try {
disposable.dispose()
} catch (err) {
console.log('err', err)
}
})
}
})
} }
});
// editor.updateOptions({ // Hacky way to hide Peek menu
// minimap: { editor.onContextMenu(e => {
// enabled: false, const host =
// }, document.querySelector<HTMLElement>(".shadow-root-host");
// ...snap.editorSettings,
// }); const contextMenuItems =
if (!state.editorCtx) { host?.shadowRoot?.querySelectorAll("li.action-item");
state.editorCtx = ref(monaco.editor) contextMenuItems?.forEach(k => {
} // If menu item contains "Peek" lets hide it
}} if (k.querySelector(".action-label")?.textContent === "Peek") {
onMount={(editor, monaco) => { // @ts-expect-error
editorRef.current = editor k["style"].display = "none";
monacoRef.current = monaco
editor.updateOptions({
glyphMargin: true,
lightbulb: {
enabled: true
} }
}) });
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { });
saveFile()
})
// When the markers (errors/warnings from clangd language server) change
// Lets improve the markers by adding extra content to them from related
// md files
monaco.editor.onDidChangeMarkers(() => {
if (monacoRef.current) {
setMarkers(monacoRef.current)
}
})
// Hacky way to hide Peek menu validateWritability(editor);
editor.onContextMenu(e => { }}
const host = document.querySelector<HTMLElement>('.shadow-root-host') theme={theme === "dark" ? "dark" : "light"}
/>
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 css={{ pl: '0px', pt: '15px', flex: 1, display: 'inline-flex' }}> <Box
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
@@ -328,7 +292,7 @@ const HooksEditor = () => {
</Container> </Container>
)} )}
</Box> </Box>
) );
} };
export default HooksEditor export default HooksEditor;

View File

@@ -1,165 +1,169 @@
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: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8' boxShadow:
} "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: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8' boxShadow:
} "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<HTMLInputElement, React.ComponentProps<typeof Input>>( const ReffedInput = React.forwardRef<
(props, ref) => <Input {...props} ref={ref} /> HTMLInputElement,
) 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'
}) })

View File

@@ -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;

View File

@@ -1,23 +1,30 @@
import { useRef, useLayoutEffect, ReactNode, FC, useState, useCallback } from 'react' import {
import { IconProps, Notepad, Prohibit } from 'phosphor-react' useRef,
import useStayScrolled from 'react-stay-scrolled' useLayoutEffect,
import NextLink from 'next/link' ReactNode,
FC,
useState,
useCallback,
} from "react";
import { IconProps, Notepad, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link";
import Container from './Container' import Container from "./Container";
import LogText from './LogText' import LogText from "./LogText";
import state, { ILog } from '../state' import state, { 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 regexifyString from "regexify-string";
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio";
import { AccountDialog } from './Accounts' 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> Icon?: FC<IconProps>;
} }
const LogBox: FC<ILogBox> = ({ const LogBox: FC<ILogBox> = ({
@@ -27,40 +34,40 @@ const LogBox: FC<ILogBox> = ({
children, children,
renderNav, renderNav,
enhanced, enhanced,
Icon = Notepad 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
@@ -68,13 +75,13 @@ 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",
}} }}
> >
<Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text> <Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
@@ -88,7 +95,7 @@ const LogBox: FC<ILogBox> = ({
> >
{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" />
@@ -103,17 +110,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) => (
@@ -121,13 +128,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} />
@@ -137,8 +144,8 @@ const LogBox: FC<ILogBox> = ({
</Box> </Box>
</Container> </Container>
</Flex> </Flex>
) );
} };
export const Log: FC<ILog> = ({ export const Log: FC<ILog> = ({
type, type,
@@ -147,21 +154,21 @@ export const Log: FC<ILog> = ({
link, link,
linkText, linkText,
defaultCollapsed, defaultCollapsed,
jsonData: _jsonData jsonData: _jsonData,
}) => { }) => {
const [expanded, setExpanded] = useState(!defaultCollapsed) const [expanded, setExpanded] = useState(!defaultCollapsed);
const { accounts } = useSnapshot(state) const { accounts } = useSnapshot(state);
const [dialogAccount, setDialogAccount] = useState<string | null>(null) const [dialogAccount, setDialogAccount] = useState<string | null>(null);
const enrichAccounts = useCallback( const enrichAccounts = useCallback(
(str?: string): ReactNode => { (str?: string): ReactNode => {
if (!str || !accounts.length) return str if (!str || !accounts.length) return null;
const pattern = `(${accounts.map(acc => acc.address).join('|')})` const pattern = `(${accounts.map(acc => acc.address).join("|")})`;
const res = regexifyString({ const res = regexifyString({
pattern: new RegExp(pattern, 'gim'), pattern: new RegExp(pattern, "gim"),
decorator: (match, idx) => { decorator: (match, idx) => {
const name = accounts.find(acc => acc.address === match)?.name const name = accounts.find(acc => acc.address === match)?.name;
return ( return (
<Link <Link
key={match + idx} key={match + idx}
@@ -172,27 +179,27 @@ export const Log: FC<ILog> = ({
> >
{name || match} {name || match}
</Link> </Link>
) );
}, },
input: str input: str,
}) });
return <>{res}</> return <>{res}</>;
}, },
[accounts] [accounts]
) );
let message: ReactNode let message: ReactNode;
if (typeof _message === 'string') { if (typeof _message === "string") {
_message = _message.trim().replace(/\n /gi, '\n') _message = _message.trim().replace(/\n /gi, "\n");
if (_message) message = enrichAccounts(_message) if (_message) message = enrichAccounts(_message);
else message = <Text muted>{'""'}</Text> else message = <Text muted>{'""'}</Text>
} else { } else {
message = _message message = _message;
} }
const jsonData = enrichAccounts(_jsonData) const jsonData = enrichAccounts(_jsonData);
return ( return (
<> <>
@@ -203,7 +210,7 @@ export const Log: FC<ILog> = ({
<LogText variant={type}> <LogText variant={type}>
{timestring && ( {timestring && (
<Text muted monospace> <Text muted monospace>
{timestring}{' '} {timestring}{" "}
</Text> </Text>
)} )}
<Pre>{message} </Pre> <Pre>{message} </Pre>
@@ -214,14 +221,14 @@ 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 /> <br />
</> </>
) );
} };
export default LogBox export default LogBox;

View File

@@ -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;

View File

@@ -1,15 +1,21 @@
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({ width, height }: { width?: string | number; height?: string | number }) { function Logo({
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"
@@ -22,7 +28,7 @@ function Logo({ width, height }: { width?: string | number; height?: string | nu
fill="#9D2DFF" fill="#9D2DFF"
/> />
</SVG> </SVG>
) );
} }
export default Logo export default Logo;

View File

@@ -1,14 +0,0 @@
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

View File

@@ -1,31 +1,31 @@
import Editor, { loader, EditorProps, Monaco } from '@monaco-editor/react' import Editor, { loader, EditorProps, Monaco } from "@monaco-editor/react";
import { CSS } from '@stitches/react' import { CSS } from "@stitches/react";
import type monaco from 'monaco-editor' import type monaco from "monaco-editor";
import { useTheme } from 'next-themes' import { useTheme } from "next-themes";
import { FC, MutableRefObject, ReactNode } from 'react' import { FC, MutableRefObject, ReactNode } from "react";
import { Flex } from '.' import { Flex } from ".";
import dark from '../theme/editor/amy.json' import dark from "../theme/editor/amy.json";
import light from '../theme/editor/xcode_default.json' import light from "../theme/editor/xcode_default.json";
export type MonacoProps = EditorProps & { export type MonacoProps = EditorProps & {
id?: string id?: string;
rootProps?: { css: CSS } & Record<string, any> rootProps?: { css: CSS } & Record<string, any>;
overlay?: ReactNode overlay?: ReactNode;
editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor> editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor>;
monacoRef?: MutableRefObject<typeof monaco> monacoRef?: MutableRefObject<typeof monaco>;
} };
loader.config({ loader.config({
paths: { paths: {
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs' vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
} },
}) });
const Monaco: FC<MonacoProps> = ({ const Monaco: FC<MonacoProps> = ({
id, id,
path = `file:///${id}`, path = `file:///${id}`,
className = id, className = id,
language = 'json', language = "json",
overlay, overlay,
editorRef, editorRef,
monacoRef, monacoRef,
@@ -33,20 +33,20 @@ const Monaco: FC<MonacoProps> = ({
rootProps, rootProps,
...rest ...rest
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme();
const setTheme = (monaco: Monaco) => { const setTheme = (monaco: Monaco) => {
monaco.editor.defineTheme('dark', dark as any) monaco.editor.defineTheme("dark", dark as any);
monaco.editor.defineTheme('light', light as any) monaco.editor.defineTheme("light", light as any);
} };
return ( return (
<Flex <Flex
fluid fluid
column column
{...rootProps} {...rootProps}
css={{ css={{
position: 'relative', position: "relative",
height: '100%', height: "100%",
...rootProps?.css ...rootProps?.css,
}} }}
> >
<Editor <Editor
@@ -54,18 +54,22 @@ const Monaco: FC<MonacoProps> = ({
language={language} language={language}
path={path} path={path}
beforeMount={monaco => { beforeMount={monaco => {
beforeMount?.(monaco) beforeMount?.(monaco);
setTheme(monaco) setTheme(monaco);
}} }}
theme={theme === 'dark' ? 'dark' : 'light'} theme={theme === "dark" ? "dark" : "light"}
{...rest} {...rest}
/> />
{overlay && ( {overlay && (
<Flex css={{ position: 'absolute', bottom: 0, right: 0, width: '100%' }}>{overlay}</Flex> <Flex
css={{ position: "absolute", bottom: 0, right: 0, width: "100%" }}
>
{overlay}
</Flex>
)} )}
</Flex> </Flex>
) );
} };
export default Monaco export default Monaco;

View File

@@ -1,90 +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";
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" />
@@ -92,30 +92,38 @@ 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 }}>{snap.gistName || 'XRPL Hooks'}</Heading> <Heading css={{ lineHeight: 1 }}>
<Text css={{ fontSize: '$xs', color: '$mauve10', lineHeight: 1 }}> {snap.files?.[0]?.name || "XRPL Hooks"}
{snap.files.length > 0 ? 'Gist: ' : 'Builder'} </Heading>
<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 || ''}/${snap.gistId || ''}`} href={`https://gist.github.com/${snap.gistOwner || ""}/${
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.gistId || '')}`} {`${snap.gistOwner || "-"}/${truncate(
snap.gistId || ""
)}`}
</Text> </Text>
</Link> </Link>
)} )}
@@ -124,8 +132,11 @@ const Navigation = () => {
)} )}
</Flex> </Flex>
{router.isReady && ( {router.isReady && (
<ButtonGroup css={{ marginLeft: 'auto' }}> <ButtonGroup css={{ marginLeft: "auto" }}>
<Dialog open={snap.mainModalOpen} onOpenChange={open => (state.mainModalOpen = open)}> <Dialog
open={snap.mainModalOpen}
onOpenChange={(open) => (state.mainModalOpen = open)}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button outline> <Button outline>
<FolderOpen size="15px" /> <FolderOpen size="15px" />
@@ -133,51 +144,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
@@ -185,27 +196,30 @@ 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 Ledger. Hooks add smart contract functionality to the XRP
Ledger.
</Text> </Text>
<Flex css={{ flexDirection: 'column', gap: '$2', mt: '$2' }}> <Flex
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"
@@ -217,16 +231,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"
@@ -237,16 +251,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"
@@ -261,28 +275,32 @@ 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'
}
}} }}
> >
{Object.values(templateFileIds).map(template => ( {Object.values(templateFileIds).map((template) => (
<PanelBox key={template.id} as="a" href={`/develop/${template.id}`}> <PanelBox
key={template.id}
as="a"
href={`/develop/${template.id}`}
>
<ImageWrapper>{template.icon()}</ImageWrapper> <ImageWrapper>{template.icon()}</ImageWrapper>
<Heading>{template.name}</Heading> <Heading>{template.name}</Heading>
@@ -294,14 +312,14 @@ const Navigation = () => {
<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" />
@@ -315,39 +333,63 @@ 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', scrollbarColor: "transparent",
scrollbarWidth: 'none' 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 href={gistId ? `/develop/${gistId}` : '/develop'} passHref shallow> <Link
<Button as="a" outline={!router.pathname.includes('/develop')} uppercase> href={gistId ? `/develop/${gistId}` : "/develop"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/develop")}
uppercase
>
Develop Develop
</Button> </Button>
</Link> </Link>
<Link href={gistId ? `/deploy/${gistId}` : '/deploy'} passHref shallow> <Link
<Button as="a" outline={!router.pathname.includes('/deploy')} uppercase> href={gistId ? `/deploy/${gistId}` : "/deploy"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
>
Deploy Deploy
</Button> </Button>
</Link> </Link>
<Link href={gistId ? `/test/${gistId}` : '/test'} passHref shallow> <Link
<Button as="a" outline={!router.pathname.includes('/test')} uppercase> href={gistId ? `/test/${gistId}` : "/test"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
>
Test Test
</Button> </Button>
</Link> </Link>
@@ -363,7 +405,7 @@ const Navigation = () => {
</Flex> </Flex>
</Container> </Container>
</Box> </Box>
) );
} };
export default Navigation export default Navigation;

View File

@@ -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;

View File

@@ -1,89 +1,92 @@
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: '0px 5px 38px -2px rgba(22, 23, 24, 1), 0px 10px 20px 0px rgba(22, 23, 24, 1)' boxShadow:
} "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<IPopover & React.ComponentProps<typeof PopoverContent>> = ({ const Popover: React.FC<
IPopover & React.ComponentProps<typeof PopoverContent>
> = ({
children, children,
content, content,
open, open,
@@ -91,12 +94,16 @@ const Popover: React.FC<IPopover & React.ComponentProps<typeof PopoverContent>>
onOpenChange, onOpenChange,
...rest ...rest
}) => ( }) => (
<PopoverRoot open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}> <PopoverRoot
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;

View File

@@ -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;

View File

@@ -1,35 +1,40 @@
import { Play, X } from 'phosphor-react' import { Play, X } from "phosphor-react";
import { HTMLInputTypeAttribute, useCallback, useEffect, useState } from 'react' import {
import state, { IAccount, IFile, ILog } from '../../state' HTMLInputTypeAttribute,
import Button from '../Button' useCallback,
import Box from '../Box' useEffect,
import Input, { Label } from '../Input' useState,
import Stack from '../Stack' } from "react";
import state, { IAccount, IFile, ILog } from "../../state";
import Button from "../Button";
import Box from "../Box";
import Input, { Label } from "../Input";
import Stack from "../Stack";
import { 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 Text from '../Text' import Text from "../Text";
import { saveFile } from '../../state/actions/saveFile' import { saveFile } from "../../state/actions/saveFile";
import { getErrors, getTags } from '../../utils/comment-parser' import { getErrors, getTags } from "../../utils/comment-parser";
import toast from 'react-hot-toast' import toast from "react-hot-toast";
const generateHtmlTemplate = (code: string, data?: Record<string, any>) => { const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
let processString: string | undefined let processString: string | undefined;
const process = { env: { NODE_ENV: 'production' } } as any const process = { env: { NODE_ENV: "production" } } as any;
if (data) { if (data) {
Object.keys(data).forEach(key => { Object.keys(data).forEach(key => {
process.env[key] = data[key] process.env[key] = data[key];
}) });
} }
processString = JSON.stringify(process) processString = JSON.stringify(process);
return ` return `
<html> <html>
@@ -61,7 +66,7 @@ const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
} }
var process = '${processString || '{}'}'; var process = '${processString || "{}"}';
process = JSON.parse(process); process = JSON.parse(process);
window.process = process window.process = process
@@ -80,107 +85,112 @@ const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
<body> <body>
</body> </body>
</html> </html>
` `;
} };
type Fields = Record< type Fields = Record<
string, string,
{ {
name: string name: string;
value: string value: string;
type?: 'Account' | `Account.${keyof IAccount}` | HTMLInputTypeAttribute type?: "Account" | `Account.${keyof IAccount}` | HTMLInputTypeAttribute;
description?: string description?: string;
required?: boolean 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 [fields, setFields] = useState<Fields>({}) const [fields, setFields] = useState<Fields>({});
const [iFrameCode, setIframeCode] = useState('') const [iFrameCode, setIframeCode] = useState("");
const [isDialogOpen, setIsDialogOpen] = useState(false) const [isDialogOpen, setIsDialogOpen] = useState(false);
const getFields = useCallback(() => { const getFields = useCallback(() => {
const inputTags = ['input', 'param', 'arg', 'argument'] const inputTags = ["input", "param", "arg", "argument"];
const tags = getTags(content) const tags = getTags(content)
.filter(tag => inputTags.includes(tag.tag)) .filter(tag => inputTags.includes(tag.tag))
.filter(tag => !!tag.name) .filter(tag => !!tag.name);
let _fields = tags.map(tag => ({ let _fields = tags.map(tag => ({
name: tag.name, name: tag.name,
value: tag.default || '', value: tag.default || "",
type: tag.type, type: tag.type,
description: tag.description, description: tag.description,
required: !tag.optional required: !tag.optional,
})) }));
const fields: Fields = _fields.reduce((acc, field) => { const fields: Fields = _fields.reduce((acc, field) => {
acc[field.name] = field acc[field.name] = field;
return acc return acc;
}, {} as Fields) }, {} as Fields);
const error = getErrors(content) const error = getErrors(content);
if (error) setTemplateError(error.message) if (error) setTemplateError(error.message);
else setTemplateError('') else setTemplateError("");
return fields return fields;
}, [content]) }, [content]);
const runScript = useCallback(() => { const runScript = useCallback(() => {
try { try {
let data: any = {} let data: any = {};
Object.keys(fields).forEach(key => { Object.keys(fields).forEach(key => {
data[key] = fields[key].value data[key] = fields[key].value;
}) });
const template = generateHtmlTemplate(content, data) const template = generateHtmlTemplate(content, data);
setIframeCode(template) setIframeCode(template);
state.scriptLogs = [{ type: 'success', message: 'Started running...' }] state.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" },
] ];
} }
}, [content, fields, snap.scriptLogs]) }, [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 defaultFields = getFields() || {} const defaultFields = getFields() || {};
setFields(defaultFields) setFields(defaultFields);
}, [content, setFields, getFields]) }, [content, setFields, getFields]);
const accOptions = snap.accounts?.map(acc => ({ const accOptions = snap.accounts?.map(acc => ({
...acc, ...acc,
label: acc.name, label: acc.name,
value: acc.address value: acc.address,
})) }));
const isDisabled = Object.values(fields).some(field => field.required && !field.value) const isDisabled = Object.values(fields).some(
field => field.required && !field.value
);
const handleRun = useCallback(() => { const handleRun = useCallback(() => {
if (isDisabled) return toast.error('Please fill in all the required fields.') if (isDisabled)
return toast.error("Please fill in all the required fields.");
state.scriptLogs = [] state.scriptLogs = [];
runScript() runScript();
setIsDialogOpen(false) setIsDialogOpen(false);
}, [isDisabled, runScript]) }, [isDisabled, runScript]);
return ( return (
<> <>
@@ -189,8 +199,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}
@@ -200,86 +210,97 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
<DialogTitle>Run {name} script</DialogTitle> <DialogTitle>Run {name} script</DialogTitle>
<DialogDescription> <DialogDescription>
<Box> <Box>
You are about to run scripts provided by the developer of the hook, make sure you You are about to run scripts provided by the developer of the
trust the author before you continue. hook, make sure you trust the author before you continue.
</Box> </Box>
{templateError && ( {templateError && (
<Box <Box
as="span" as="span"
css={{ css={{
display: 'block', display: "block",
color: '$error', color: "$error",
mt: '$3', mt: "$3",
whiteSpace: 'pre' whiteSpace: "pre",
}} }}
> >
{templateError} {templateError}
</Box> </Box>
)} )}
{Object.keys(fields).length > 0 && ( {Object.keys(fields).length > 0 && (
<Box css={{ mt: '$4', mb: 0 }}> <Box css={{ mt: "$4", mb: 0 }}>
Fill in the following parameters to run the script. Fill in the following parameters to run the script.
</Box> </Box>
)} )}
</DialogDescription> </DialogDescription>
<Stack css={{ width: '100%' }}> <Stack css={{ width: "100%" }}>
{Object.keys(fields).map(key => { {Object.keys(fields).map(key => {
const { name, value, type, description, required } = fields[key] const { name, value, type, description, required } = fields[key];
const isAccount = type?.startsWith('Account') const isAccount = type?.startsWith("Account");
const isAccountSecret = type === 'Account.secret' const isAccountSecret = type === "Account.secret";
const accountField = (isAccount && type?.split('.')[1]) || 'address' const accountField =
(isAccount && type?.split(".")[1]) || "address";
return ( return (
<Box key={name} css={{ width: '100%' }}> <Box key={name} css={{ width: "100%" }}>
<Label css={{ display: 'flex', justifyContent: 'space-between' }}> <Label
css={{ display: "flex", justifyContent: "space-between" }}
>
<span> <span>
{description || name} {required && <Text error>*</Text>} {description || name} {required && <Text error>*</Text>}
</span> </span>
{isAccountSecret && ( {isAccountSecret && (
<Text error small css={{ alignSelf: 'end' }}> <Text error small css={{ alignSelf: "end" }}>
can access account secret key can access account secret key
</Text> </Text>
)} )}
</Label> </Label>
{isAccount ? ( {isAccount ? (
<Select <Select
css={{ mt: '$1' }} css={{ mt: "$1" }}
options={accOptions} options={accOptions}
onChange={(val: any) => { onChange={(val: any) => {
setFields({ setFields({
...fields, ...fields,
[key]: { [key]: {
...fields[key], ...fields[key],
value: val[accountField] value: val[accountField],
} },
}) });
}} }}
value={accOptions.find((acc: any) => acc[accountField] === value)} value={accOptions.find(
(acc: any) => acc[accountField] === value
)}
/> />
) : ( ) : (
<Input <Input
type={type || 'text'} type={type || "text"}
value={value} value={value}
css={{ mt: '$1' }} css={{ mt: "$1" }}
onChange={e => { onChange={e => {
setFields({ setFields({
...fields, ...fields,
[key]: { ...fields[key], value: e.target.value } [key]: { ...fields[key], value: e.target.value },
}) });
}} }}
/> />
)} )}
</Box> </Box>
) );
})} })}
<Flex css={{ justifyContent: 'flex-end', width: '100%', gap: '$3' }}> <Flex
css={{ justifyContent: "flex-end", width: "100%", gap: "$3" }}
>
<DialogClose asChild> <DialogClose asChild>
<Button outline>Cancel</Button> <Button outline>Cancel</Button>
</DialogClose> </DialogClose>
<Button variant="primary" isDisabled={isDisabled} onClick={handleRun}> <Button
variant="primary"
isDisabled={isDisabled}
onClick={handleRun}
>
Run script Run script
</Button> </Button>
</Flex> </Flex>
@@ -287,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" />
@@ -303,10 +324,14 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{iFrameCode && ( {iFrameCode && (
<iframe style={{ display: 'none' }} srcDoc={iFrameCode} sandbox="allow-scripts" /> <iframe
style={{ display: "none" }}
srcDoc={iFrameCode}
sandbox="allow-scripts"
/>
)} )}
</> </>
) );
} };
export default RunScript export default RunScript;

View File

@@ -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,97 +26,102 @@ 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",
} };
}, },
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 ${state.isFocused ? colors.border : colors.secondary}` boxShadow: `0 0 0 1px ${
} 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: state.isFocused ? colors.activeLight : colors.dropDownBg, backgroundColor:
':hover': { state.isFocused
? 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, {});

View File

@@ -1,65 +1,77 @@
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, Box, Text } from '.' import { Button, Box, Text } from ".";
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 { Controller, SubmitHandler, useFieldArray, useForm } from 'react-hook-form' import {
Controller,
SubmitHandler,
useFieldArray,
useForm,
} from "react-hook-form";
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, { IFile, SelectOption } 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 {
import { capitalize } from '../utils/helpers' getParameters,
getInvokeOptions,
transactionOptions,
SetHookData,
} from "../utils/setHook";
import { capitalize } from "../utils/helpers";
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 [estimateLoading, setEstimateLoading] = useState(false) const [estimateLoading, setEstimateLoading] = useState(false);
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false) const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const compiledFiles = snap.files.filter(file => file.compiledContent) const compiledFiles = snap.files.filter(file => file.compiledContent);
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({ const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
label: acc.name, label: acc.name,
value: acc.address value: acc.address,
})) }));
const [selectedAccount, setSelectedAccount] = useState( const [selectedAccount, setSelectedAccount] = useState(
accountOptions.find(acc => acc.value === accountAddress) accountOptions.find(acc => acc.value === accountAddress)
) );
const account = snap.accounts.find(acc => acc.address === selectedAccount?.value) const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value
);
const getHookNamespace = useCallback( const getHookNamespace = useCallback(
() => () =>
(activeFile && snap.deployValues[activeFile.name]?.HookNamespace) || (activeFile && snap.deployValues[activeFile.name]?.HookNamespace) ||
activeFile?.name.split('.')[0] || activeFile?.name.split(".")[0] ||
'', "",
[activeFile, snap.deployValues] [activeFile, snap.deployValues]
) );
const getDefaultValues = useCallback((): Partial<SetHookData> => { const getDefaultValues = useCallback((): Partial<SetHookData> => {
const content = activeFile?.compiledValueSnapshot const content = activeFile?.compiledValueSnapshot;
return ( return (
(activeFile && snap.deployValues[activeFile.name]) || { (activeFile && snap.deployValues[activeFile.name]) || {
HookNamespace: getHookNamespace(), HookNamespace: getHookNamespace(),
Invoke: getInvokeOptions(content), Invoke: getInvokeOptions(content),
HookParameters: getParameters(content) HookParameters: getParameters(content),
} }
) );
}, [activeFile, getHookNamespace, snap.deployValues]) }, [activeFile, getHookNamespace, snap.deployValues]);
const { const {
register, register,
@@ -69,30 +81,33 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
setValue, setValue,
getValues, getValues,
reset, reset,
formState: { errors } formState: { errors },
} = useForm<SetHookData>({ } = useForm<SetHookData>({
defaultValues: getDefaultValues() defaultValues: getDefaultValues(),
}) });
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 watchedFee = watch('Fee') const watchedFee = watch("Fee");
// Reset form if activeFile changes // Reset form if activeFile changes
useEffect(() => { useEffect(() => {
if (!activeFile) return if (!activeFile) return;
const defaultValues = getDefaultValues() const defaultValues = getDefaultValues();
reset(defaultValues) reset(defaultValues);
}, [activeFile, getDefaultValues, reset]) }, [activeFile, getDefaultValues, reset]);
useEffect(() => { useEffect(() => {
if (watchedFee && (watchedFee.includes('.') || watchedFee.includes(','))) { if (
setValue('Fee', watchedFee.replaceAll('.', '').replaceAll(',', '')) watchedFee &&
(watchedFee.includes(".") || watchedFee.includes(","))
) {
setValue("Fee", watchedFee.replaceAll(".", "").replaceAll(",", ""));
} }
}, [watchedFee, setValue]) }, [watchedFee, setValue]);
// const { // const {
// fields: grantFields, // fields: grantFields,
// append: grantAppend, // append: grantAppend,
@@ -101,67 +116,67 @@ 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', getHookNamespace()) const namespace = watch("HookNamespace", getHookNamespace());
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(() => { useEffect(() => {
calculateHashedValue() calculateHashedValue();
}, [namespace, calculateHashedValue]) }, [namespace, calculateHashedValue]);
const calculateFee = useCallback(async () => { const calculateFee = useCallback(async () => {
if (!account) return if (!account) return;
const formValues = getValues() const formValues = getValues();
const tx = await prepareDeployHookTx(account, formValues) const tx = await prepareDeployHookTx(account, formValues);
if (!tx) { if (!tx) {
return return;
} }
const res = await estimateFee(tx, account) const res = await estimateFee(tx, account);
if (res && res.base_fee) { if (res && res.base_fee) {
setValue('Fee', Math.round(Number(res.base_fee || '')).toString()) setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
} }
}, [account, getValues, setValue]) }, [account, getValues, setValue]);
const tooLargeFile = () => { const tooLargeFile = () => {
return Boolean( return Boolean(
activeFile?.compiledContent?.byteLength && activeFile?.compiledContent?.byteLength >= 64000 activeFile?.compiledContent?.byteLength &&
) activeFile?.compiledContent?.byteLength >= 64000
} );
};
const onSubmit: SubmitHandler<SetHookData> = async data => { const onSubmit: SubmitHandler<SetHookData> = async data => {
const currAccount = state.accounts.find(acc => acc.address === account?.address) const currAccount = state.accounts.find(
if (!account) return acc => acc.address === account?.address
if (currAccount) currAccount.isLoading = true );
if (!account) return;
if (currAccount) currAccount.isLoading = true;
data.HookParameters.forEach(param => { data.HookParameters.forEach(param => {
delete param.$metaData delete param.$metaData;
return param return param;
}) });
const res = await deployHook(account, data) const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false if (currAccount) currAccount.isLoading = false;
if (res && res.engine_result === 'tesSUCCESS') { if (res && res.engine_result === "tesSUCCESS") {
toast.success('Transaction succeeded!') toast.success("Transaction succeeded!");
return setIsSetHookDialogOpen(false) return setIsSetHookDialogOpen(false);
} }
toast.error(`Transaction failed! (${res?.engine_result_message})`) toast.error(`Transaction failed! (${res?.engine_result_message})`);
} };
const onOpenChange = useCallback( const onOpenChange = useCallback((open: boolean) => {
(open: boolean) => { setIsSetHookDialogOpen(open);
setIsSetHookDialogOpen(open)
if (open) calculateFee() if (open) calculateFee();
}, }, [calculateFee]);
[calculateFee]
)
return ( return (
<Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}> <Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -169,8 +184,10 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
ghost ghost
size="xs" size="xs"
uppercase uppercase
variant={'secondary'} variant={"secondary"}
disabled={!account || account.isLoading || !activeFile || tooLargeFile()} disabled={
!account || account.isLoading || !activeFile || tooLargeFile()
}
> >
Set Hook Set Hook
</Button> </Button>
@@ -179,8 +196,8 @@ 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> <Label>Account</Label>
<Select <Select
instanceId="deploy-account" instanceId="deploy-account"
@@ -190,7 +207,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
onChange={(acc: any) => setSelectedAccount(acc)} onChange={(acc: any) => setSelectedAccount(acc)}
/> />
</Box> </Box>
<Box css={{ width: '100%' }}> <Box css={{ width: "100%" }}>
<Label>Invoke on transactions</Label> <Label>Invoke on transactions</Label>
<Controller <Controller
name="Invoke" name="Invoke"
@@ -206,20 +223,27 @@ 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 {...register('HookNamespace', { required: true })} autoComplete={'off'} /> <Input
{errors.HookNamespace?.type === 'required' && ( {...register("HookNamespace", { required: true })}
<Box css={{ display: 'inline', color: '$red11' }}>Namespace is required</Box> autoComplete={"off"}
/>
{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' }}>Hook parameters</Label> <Label style={{ marginBottom: "10px", display: "block" }}>
Hook parameters
</Label>
<Stack> <Stack>
{fields.map((field, index) => ( {fields.map((field, index) => (
<Stack key={field.id}> <Stack key={field.id}>
@@ -234,20 +258,25 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
)} )}
/> />
<Input <Input
css={{ mx: '$2' }} css={{ mx: "$2" }}
placeholder="Value (hex-quoted)" placeholder="Value (hex-quoted)"
{...register( {...register(
`HookParameters.${index}.HookParameter.HookParameterValue`, `HookParameters.${index}.HookParameter.HookParameterValue`,
{ required: field.$metaData?.required } { required: field.$metaData?.required }
)} )}
/> />
<Button onClick={() => remove(index)} variant="destroy"> <Button
onClick={() => remove(index)}
variant="destroy"
>
<Trash weight="regular" size="16px" /> <Trash weight="regular" size="16px" />
</Button> </Button>
</Flex> </Flex>
{errors.HookParameters?.[index]?.HookParameter?.HookParameterValue {errors.HookParameters?.[index]?.HookParameter
?.type === 'required' && <Text error>This field is required</Text>} ?.HookParameterValue?.type === "required" && (
<Label css={{ fontSize: '$sm', mt: '$1' }}> <Text error>This field is required</Text>
)}
<Label css={{ fontSize: "$sm", mt: "$1" }}>
{capitalize(field.$metaData?.description)} {capitalize(field.$metaData?.description)}
</Label> </Label>
</Flex> </Flex>
@@ -260,9 +289,9 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
onClick={() => onClick={() =>
append({ append({
HookParameter: { HookParameter: {
HookParameterName: '', HookParameterName: "",
HookParameterValue: '' HookParameterValue: "",
} },
}) })
} }
> >
@@ -271,30 +300,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
@@ -303,37 +332,47 @@ 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();
if (!account) return if (!account) return;
setEstimateLoading(true) setEstimateLoading(true);
const formValues = getValues() const formValues = getValues();
try { try {
const tx = await prepareDeployHookTx(account, formValues) const tx = await prepareDeployHookTx(
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('Fee', Math.round(Number(res.base_fee || '')).toString()) setValue(
"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' }}>Fee is required</Box> <Box css={{ display: "inline", color: "$red11" }}>
Fee is required
</Box>
)} )}
</Box> </Box>
{/* <Box css={{ width: "100%" }}> {/* <Box css={{ width: "100%" }}>
@@ -390,31 +429,35 @@ 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 variant="primary" type="submit" isLoading={account?.isLoading}> <Button
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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,58 +1,63 @@
import React, { useEffect, useState, Fragment, isValidElement, useCallback } from 'react' import React, {
import type { ReactNode, ReactElement } from 'react' useEffect,
import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from '.' useState,
Fragment,
isValidElement,
useCallback,
} from "react";
import type { ReactNode, ReactElement } from "react";
import { Box, Button, Flex, Input, Label, Pre, 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 } from '../utils/helpers' import { capitalize } from "../utils/helpers";
import ContextMenu, { ContentMenuOption } from './ContextMenu' 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 type Nullable<T> = T | null | undefined | false;
interface TabProps { interface TabProps {
header: string header: string;
children?: ReactNode children?: ReactNode;
renameDisabled?: boolean
} }
// TODO customize messages shown // TODO customize messages shown
interface Props { interface Props {
label?: string label?: string;
activeIndex?: number activeIndex?: number;
activeHeader?: string activeHeader?: string;
headless?: boolean headless?: boolean;
children: ReactElement<TabProps>[] children: ReactElement<TabProps>[];
keepAllAlive?: boolean keepAllAlive?: boolean;
defaultExtension?: string defaultExtension?: string;
extensionRequired?: boolean extensionRequired?: boolean;
allowedExtensions?: string[] allowedExtensions?: string[];
headerExtraValidation?: { headerExtraValidation?: {
regex: string | RegExp regex: string | RegExp;
error: string error: string;
} };
onCreateNewTab?: (name: string) => any onCreateNewTab?: (name: string) => any;
onRenameTab?: (index: number, nwName: string, oldName?: string) => any onRenameTab?: (index: number, nwName: string, oldName?: string) => any;
onCloseTab?: (index: number, header?: string) => any onCloseTab?: (index: number, header?: string) => any;
onChangeActive?: (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', label = "Tab",
children, children,
activeIndex, activeIndex,
activeHeader, activeHeader,
@@ -64,155 +69,165 @@ export const Tabs = ({
onRenameTab, onRenameTab,
headerExtraValidation, headerExtraValidation,
extensionRequired, extensionRequired,
defaultExtension = '', defaultExtension = "",
allowedExtensions 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 [renamingTab, setRenamingTab] = useState<number | null>(null) const [renamingTab, setRenamingTab] = useState<number | null>(null);
const [tabname, setTabname] = useState('') const [tabname, setTabname] = useState("");
const [tabnameError, setTabnameError] = useState<string | null>(null) 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(() => {
setTabnameError(null) setTabnameError(null);
}, [tabname, setTabnameError]) }, [tabname, setTabnameError]);
const validateTabname = useCallback( const validateTabname = useCallback(
(tabname: string): { error?: string; result?: string } => { (tabname: string): { error?: string, result?: string } => {
if (!tabname) { if (!tabname) {
return { error: `Please enter ${label.toLocaleLowerCase()} name.` } return { error: `Please enter ${label.toLocaleLowerCase()} name.` };
} }
let ext = (tabname.includes('.') && tabname.split('.').pop()) || '' let ext =
(tabname.includes(".") && tabname.split(".").pop()) || "";
if (!ext && defaultExtension) { if (!ext && defaultExtension) {
ext = defaultExtension ext = defaultExtension
tabname = `${tabname}.${defaultExtension}` tabname = `${tabname}.${defaultExtension}`
} }
if (tabs.find(tab => tab.header === tabname)) { if (tabs.find(tab => tab.header === tabname)) {
return { error: `${capitalize(label)} name already exists.` } return { error: `${capitalize(label)} name already exists.` };
} }
if (extensionRequired && !ext) { if (extensionRequired && !ext) {
return { error: 'File extension is required!' } return { error: "File extension is required!" };
} }
if (allowedExtensions && !allowedExtensions.includes(ext)) { if (allowedExtensions && !allowedExtensions.includes(ext)) {
return { error: 'This file extension is not allowed!' } return { error: "This file extension is not allowed!" };
} }
if (headerExtraValidation && !tabname.match(headerExtraValidation.regex)) { if (
return { error: headerExtraValidation.error } headerExtraValidation &&
!tabname.match(headerExtraValidation.regex)
) {
return { error: headerExtraValidation.error };
} }
return { result: tabname } return { result: tabname };
}, },
[allowedExtensions, defaultExtension, extensionRequired, headerExtraValidation, label, 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(() => { const handleRenameTab = useCallback(() => {
if (renamingTab === null) return if (renamingTab === null) return;
const res = validateTabname(tabname) const res = validateTabname(tabname);
if (res.error) { if (res.error) {
setTabnameError(`Error: ${res.error}`) setTabnameError(`Error: ${res.error}`);
return return;
} }
const { result: nwName = tabname } = res const { result: _tabname = tabname } = res
setRenamingTab(null) setRenamingTab(null);
setTabname('') setTabname("");
const oldName = tabs[renamingTab]?.header const oldName = tabs[renamingTab]?.header;
onRenameTab?.(renamingTab, nwName, oldName) onRenameTab?.(renamingTab, _tabname, oldName);
handleActiveChange(renamingTab, nwName) handleActiveChange(renamingTab);
}, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname]) }, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname]);
const handleCreateTab = useCallback(() => { const handleCreateTab = useCallback(() => {
const res = validateTabname(tabname) const res = validateTabname(tabname);
if (res.error) { if (res.error) {
setTabnameError(`Error: ${res.error}`) setTabnameError(`Error: ${res.error}`);
return return;
} }
const { result: _tabname = tabname } = res const { result: _tabname = tabname } = res
setIsNewtabDialogOpen(false) setIsNewtabDialogOpen(false);
setTabname('') setTabname("");
onCreateNewTab?.(_tabname) onCreateNewTab?.(_tabname);
handleActiveChange(tabs.length, _tabname) handleActiveChange(tabs.length, _tabname);
}, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length]) }, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length]);
const handleCloseTab = useCallback( const handleCloseTab = useCallback(
(idx: number) => { (idx: number) => {
onCloseTab?.(idx, tabs[idx].header)
if (idx <= active && active !== 0) { if (idx <= active && active !== 0) {
const nwActive = active - 1 setActive(active - 1);
handleActiveChange(nwActive, tabs[nwActive].header)
} }
onCloseTab?.(idx, tabs[idx].header);
handleActiveChange(idx, tabs[idx].header);
}, },
[active, handleActiveChange, onCloseTab, tabs] [active, handleActiveChange, onCloseTab, tabs]
) );
const closeOption = (idx: number): Nullable<ContentMenuOption> => const closeOption = (idx: number): Nullable<ContentMenuOption> =>
onCloseTab && { onCloseTab && {
type: 'text', type: "text",
label: 'Close', label: "Close",
key: 'close', key: "close",
onSelect: () => handleCloseTab(idx) onSelect: () => handleCloseTab(idx),
} };
const renameOption = (idx: number, tab: TabProps): Nullable<ContentMenuOption> => { const renameOption = (idx: number): Nullable<ContentMenuOption> =>
return ( onRenameTab && {
onRenameTab && type: "text",
!tab.renameDisabled && { label: "Rename",
type: 'text', key: "rename",
label: 'Rename', onSelect: () => setRenamingTab(idx),
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) => (
<ContextMenu <ContextMenu
key={tab.header} key={tab.header}
options={ options={
[closeOption(idx), renameOption(idx, tab)].filter(Boolean) as ContentMenuOption[] [closeOption(idx), renameOption(idx)].filter(
Boolean
) as ContentMenuOption[]
} }
> >
<Button <Button
@@ -223,11 +238,11 @@ export const Tabs = ({
outline={active !== idx} outline={active !== idx}
size="sm" size="sm"
css={{ css={{
'&:hover': { "&:hover": {
span: { span: {
visibility: 'visible' visibility: "visible",
} },
} },
}} }}
> >
{tab.header || idx} {tab.header || idx}
@@ -235,19 +250,19 @@ export const Tabs = ({
<Box <Box
as="span" as="span"
css={{ css={{
display: 'flex', display: "flex",
p: '2px', p: "2px",
borderRadius: '$full', borderRadius: "$full",
mr: '-4px', mr: "-4px",
'&:hover': { "&:hover": {
// boxSizing: "0px 0px 1px", // boxSizing: "0px 0px 1px",
backgroundColor: '$mauve2', backgroundColor: "$mauve2",
color: '$mauve12' color: "$mauve12",
} },
}} }}
onClick={(ev: React.MouseEvent<HTMLElement>) => { onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation() ev.stopPropagation();
handleCloseTab(idx) handleCloseTab(idx);
}} }}
> >
<X size="9px" weight="bold" /> <X size="9px" weight="bold" />
@@ -257,22 +272,32 @@ export const Tabs = ({
</ContextMenu> </ContextMenu>
))} ))}
{onCreateNewTab && ( {onCreateNewTab && (
<Dialog open={isNewtabDialogOpen} onOpenChange={setIsNewtabDialogOpen}> <Dialog
open={isNewtabDialogOpen}
onOpenChange={setIsNewtabDialogOpen}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button ghost size="sm" css={{ alignItems: 'center', px: '$2', mr: '$3' }}> <Button
<Plus size="16px" /> {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`} ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" />{" "}
{tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle> <DialogTitle>
Create new {label.toLocaleLowerCase()}
</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>{label} name</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();
} }
}} }}
/> />
@@ -282,8 +307,8 @@ export const Tabs = ({
<Flex <Flex
css={{ css={{
marginTop: 25, marginTop: 25,
justifyContent: 'flex-end', justifyContent: "flex-end",
gap: '$3' gap: "$3",
}} }}
> >
<DialogClose asChild> <DialogClose asChild>
@@ -294,7 +319,7 @@ 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" /> <X size="20px" />
</Box> </Box>
</DialogClose> </DialogClose>
@@ -302,7 +327,10 @@ export const Tabs = ({
</Dialog> </Dialog>
)} )}
{onRenameTab && ( {onRenameTab && (
<Dialog open={renamingTab !== null} onOpenChange={() => setRenamingTab(null)}> <Dialog
open={renamingTab !== null}
onOpenChange={() => setRenamingTab(null)}
>
<DialogContent> <DialogContent>
<DialogTitle> <DialogTitle>
Rename <Pre>{tabs[renamingTab || 0]?.header}</Pre> Rename <Pre>{tabs[renamingTab || 0]?.header}</Pre>
@@ -313,8 +341,8 @@ export const Tabs = ({
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") {
handleRenameTab() handleRenameTab();
} }
}} }}
/> />
@@ -324,8 +352,8 @@ export const Tabs = ({
<Flex <Flex
css={{ css={{
marginTop: 25, marginTop: 25,
justifyContent: 'flex-end', justifyContent: "flex-end",
gap: '$3' gap: "$3",
}} }}
> >
<DialogClose asChild> <DialogClose asChild>
@@ -336,7 +364,7 @@ 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" /> <X size="20px" />
</Box> </Box>
</DialogClose> </DialogClose>
@@ -349,26 +377,28 @@ export const Tabs = ({
? 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={{
...style, ...style,
display: active !== idx ? 'none' : undefined display: active !== idx ? "none" : undefined,
}} }}
/> />
) );
}) })
: tabs[active] && ( : tabs[active] && (
<Fragment key={tabs[active].header || active}>{tabs[active].children}</Fragment> <Fragment key={tabs[active].header || active}>
{tabs[active].children}
</Fragment>
)} )}
</> </>
) );
} };

View File

@@ -1,41 +1,41 @@
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: {
fontSize: '$xs' fontSize: "$xs",
} },
}, },
muted: { muted: {
true: { true: {
color: '$mauve9' color: "$mauve9",
} },
}, },
error: { error: {
true: { true: {
color: '$error' color: "$error",
} },
}, },
warning: { warning: {
true: { true: {
color: '$warning' color: "$warning",
} },
}, },
monospace: { monospace: {
true: { true: {
fontFamily: '$monospace' fontFamily: "$monospace",
} },
}, },
block: { block: {
true: { true: {
display: 'block' display: "block",
} },
} },
} },
}) });
export default Text export default Text;

View File

@@ -1,113 +1,115 @@
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: 'inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8' boxShadow:
} "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: 'inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8' boxShadow:
} "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;

View File

@@ -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;

View File

@@ -1,69 +1,72 @@
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: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px', boxShadow:
'@media (prefers-reduced-motion: no-preference)': { "hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
animationDuration: '400ms', "@media (prefers-reduced-motion: no-preference)": {
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', animationDuration: "400ms",
animationFillMode: 'forwards', animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
willChange: 'transform, opacity', animationFillMode: "forwards",
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<React.ComponentProps<typeof StyledContent> & ITooltip> = ({ const Tooltip: React.FC<
React.ComponentProps<typeof StyledContent> & ITooltip
> = ({
children, children,
content, content,
open, open,
@@ -72,14 +75,18 @@ const Tooltip: React.FC<React.ComponentProps<typeof StyledContent> & ITooltip> =
...rest ...rest
}) => { }) => {
return ( return (
<TooltipPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}> <TooltipPrimitive.Root
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger> <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<StyledContent side="bottom" align="center" {...rest}> <StyledContent side="bottom" align="center" {...rest}>
<div dangerouslySetInnerHTML={{ __html: content }} /> <div dangerouslySetInnerHTML={{ __html: content }} />
<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;

View File

@@ -1,7 +1,7 @@
import { Play } from 'phosphor-react' import { Play } from "phosphor-react";
import { FC, useCallback, useEffect } 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 {
defaultTransactionType, defaultTransactionType,
getTxFields, getTxFields,
@@ -9,100 +9,122 @@ import {
prepareState, prepareState,
prepareTransaction, prepareTransaction,
SelectOption, SelectOption,
TransactionState TransactionState,
} from '../../state/transactions' } from "../../state/transactions";
import { sendTransaction } from '../../state/actions' import { sendTransaction } from "../../state/actions";
import Box from '../Box' import Box from "../Box";
import Button from '../Button' import Button from "../Button";
import Flex from '../Flex' import Flex from "../Flex";
import { TxJson } from './json' import { TxJson } from "./json";
import { TxUI } from './ui' import { TxUI } from "./ui";
import { default as _estimateFee } from '../../utils/estimateFee' import { default as _estimateFee } from "../../utils/estimateFee";
import toast from 'react-hot-toast' import toast from "react-hot-toast";
export interface TransactionProps { export interface TransactionProps {
header: string header: string;
state: TransactionState state: TransactionState;
} }
const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props }) => { const Transaction: FC<TransactionProps> = ({
const { accounts, editorSettings } = useSnapshot(state) header,
const { selectedAccount, selectedTransaction, txIsDisabled, txIsLoading, viewType, editorValue } = state: txState,
txState ...props
}) => {
const { accounts, editorSettings } = useSnapshot(state);
const {
selectedAccount,
selectedTransaction,
txIsDisabled,
txIsLoading,
viewType,
editorValue,
} = txState;
const setState = useCallback( const setState = useCallback(
(pTx?: Partial<TransactionState>) => { (pTx?: Partial<TransactionState>) => {
return modifyTxState(header, pTx) return modifyTxState(header, pTx);
}, },
[header] [header]
) );
const prepareOptions = useCallback( const prepareOptions = useCallback(
(state: Partial<TransactionState> = txState) => { (state: Partial<TransactionState> = txState) => {
const { selectedTransaction, selectedDestAccount, selectedAccount, txFields } = state const {
selectedTransaction,
selectedDestAccount,
selectedAccount,
txFields,
} = state;
const TransactionType = selectedTransaction?.value || null const TransactionType = selectedTransaction?.value || null;
const Destination = selectedDestAccount?.value || txFields?.Destination const Destination = selectedDestAccount?.value || txFields?.Destination;
const Account = selectedAccount?.value || null const Account = selectedAccount?.value || null;
return prepareTransaction({ return prepareTransaction({
...txFields, ...txFields,
TransactionType, TransactionType,
Destination, Destination,
Account Account,
}) });
}, },
[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,
setState,
txIsLoading,
]);
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 // save the editor state first
const pst = prepareState(editorValue || '', tt) const pst = prepareState(editorValue || "", tt);
if (!pst) return if (!pst) return;
st = setState(pst) st = setState(pst);
} }
const account = accounts.find(acc => acc.address === selectedAccount?.value) const account = accounts.find(
if (txIsDisabled) return acc => acc.address === selectedAccount?.value
);
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);
const fields = getTxFields(options.TransactionType) const fields = getTxFields(options.TransactionType);
if (fields.Destination && !options.Destination) { if (fields.Destination && !options.Destination) {
throw Error('Destination account is required!') 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 });
}, [ }, [
viewType, viewType,
accounts, accounts,
@@ -112,65 +134,73 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
editorValue, editorValue,
txState, txState,
selectedAccount?.value, selectedAccount?.value,
prepareOptions prepareOptions,
]) ]);
const getJsonString = useCallback( const getJsonString = useCallback(
(state?: Partial<TransactionState>) => (state?: Partial<TransactionState>) =>
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize), JSON.stringify(
prepareOptions?.(state) || {},
null,
editorSettings.tabSize
),
[editorSettings.tabSize, prepareOptions] [editorSettings.tabSize, prepareOptions]
) );
const resetState = useCallback( const resetState = useCallback(
(transactionType: SelectOption | undefined = defaultTransactionType) => { (transactionType: SelectOption | undefined = defaultTransactionType) => {
const fields = getTxFields(transactionType?.value) const fields = getTxFields(transactionType?.value);
const nwState: Partial<TransactionState> = { const nwState: Partial<TransactionState> = {
viewType, viewType,
selectedTransaction: transactionType selectedTransaction: transactionType,
} selectedDestAccount: null
};
// Currently in schema "Destination": "SomeVal" means 'Destination is required' while empty string indicates it is optional
// TODO Update schema with clear required tag
if (fields.Destination !== undefined) { if (fields.Destination !== undefined) {
nwState.selectedDestAccount = null fields.Destination = "";
fields.Destination = ''
} else { } else {
fields.Destination = undefined fields.Destination = undefined;
} }
nwState.txFields = fields nwState.txFields = fields;
const state = modifyTxState(header, nwState, { replaceState: true }) const state = modifyTxState(header, nwState, { replaceState: true });
const editorValue = getJsonString(state) const editorValue = getJsonString(state);
return setState({ editorValue }) return setState({ editorValue });
}, },
[getJsonString, header, setState, viewType] [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(acc => acc.address === state.selectedAccount?.value) const account = accounts.find(
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]
) );
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
getJsonString={getJsonString} getJsonString={getJsonString}
header={header} header={header}
@@ -184,26 +214,26 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
<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({ viewType: 'json' }) setState({ viewType: "json" });
} else setState({ viewType: 'ui' }) } else setState({ viewType: "ui" });
}} }}
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
@@ -218,7 +248,7 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
</Flex> </Flex>
</Flex> </Flex>
</Box> </Box>
) );
} };
export default Transaction export default Transaction;

View File

@@ -1,176 +1,192 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useState } from "react";
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio";
import state, { prepareState, transactionsData, TransactionState } from '../../state' import state, {
import Text from '../Text' prepareState,
import { Flex, Link } from '..' transactionsData,
import { showAlert } from '../../state/actions/showAlert' TransactionState,
import { parseJSON } from '../../utils/json' } from "../../state";
import { extractSchemaProps } from '../../utils/schema' import Text from "../Text";
import amountSchema from '../../content/amount-schema.json' import { Flex, Link } from "..";
import Monaco from '../Monaco' import { showAlert } from "../../state/actions/showAlert";
import type monaco from 'monaco-editor' import { parseJSON } from "../../utils/json";
import { extractSchemaProps } from "../../utils/schema";
import amountSchema from "../../content/amount-schema.json";
import Monaco from "../Monaco";
import type monaco from "monaco-editor";
interface JsonProps { interface JsonProps {
getJsonString?: (state?: Partial<TransactionState>) => string getJsonString?: (state?: Partial<TransactionState>) => string;
header?: string header?: string;
setState: (pTx?: Partial<TransactionState> | undefined) => void setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState state: TransactionState;
estimateFee?: () => Promise<string | undefined> estimateFee?: () => Promise<string | undefined>;
} }
export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, setState }) => { export const TxJson: FC<JsonProps> = ({
const { editorSettings, accounts } = useSnapshot(state) getJsonString,
const { editorValue, estimatedFee } = txState state: txState,
header,
setState,
}) => {
const { editorSettings, accounts } = useSnapshot(state);
const { editorValue, estimatedFee } = txState;
const [currTxType, setCurrTxType] = useState<string | undefined>( const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value txState.selectedTransaction?.value
) );
useEffect(() => { useEffect(() => {
setState({ setState({
editorValue: getJsonString?.() editorValue: getJsonString?.(),
}) });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, []);
useEffect(() => { useEffect(() => {
const parsed = parseJSON(editorValue) const parsed = parseJSON(editorValue);
if (!parsed) return if (!parsed) return;
const tt = parsed.TransactionType const tt = parsed.TransactionType;
const tx = transactionsData.find(t => t.TransactionType === tt) const tx = transactionsData.find(t => t.TransactionType === tt);
if (tx) setCurrTxType(tx.TransactionType) if (tx) setCurrTxType(tx.TransactionType);
else { else {
setCurrTxType(undefined) setCurrTxType(undefined);
} }
}, [editorValue]) }, [editorValue]);
const saveState = (value: string, transactionType?: string) => { const saveState = (value: string, transactionType?: string) => {
const tx = prepareState(value, transactionType) const tx = prepareState(value, transactionType);
if (tx) { if (tx) {
setState(tx) setState(tx);
setState({ setState({
editorValue: getJsonString?.(tx) editorValue: getJsonString?.(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",
onCancel: () => {}, onCancel: () => {},
onConfirm: () => setState({ editorValue: getJsonString?.() }) 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) saveState(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: getJsonString?.() }), onConfirm: () => setState({ editorValue: getJsonString?.() }),
onCancel: () => setState({ viewType: 'json' }) onCancel: () => setState({ viewType: "json" }),
}) });
} };
const getSchemas = useCallback(async (): Promise<any[]> => { const getSchemas = useCallback(async (): Promise<any[]> => {
const txObj = transactionsData.find(td => td.TransactionType === currTxType) const txObj = transactionsData.find(
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 ? 'Above mentioned value is recommended base fee' : undefined description: estimatedFee
} ? "Above mentioned value is recommended base fee"
: undefined,
},
}, },
{ {
...amountSchema ...amountSchema,
} },
] ];
}, [accounts, currTxType, estimatedFee, header]) }, [accounts, currTxType, estimatedFee, header]);
const [monacoInst, setMonacoInst] = useState<typeof monaco>() const [monacoInst, setMonacoInst] = useState<typeof monaco>();
useEffect(() => { useEffect(() => {
if (!monacoInst) return if (!monacoInst) return;
getSchemas().then(schemas => { getSchemas().then(schemas => {
monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({ monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true, validate: true,
schemas schemas,
}) });
}) });
}, [getSchemas, monacoInst]) }, [getSchemas, monacoInst]);
const hasUnsaved = useMemo(() => editorValue !== getJsonString?.(), [editorValue, getJsonString]) const hasUnsaved = useMemo(
() => editorValue !== getJsonString?.(),
[editorValue, getJsonString]
);
return ( return (
<Monaco <Monaco
rootProps={{ rootProps={{
css: { height: 'calc(100% - 45px)' } css: { height: "calc(100% - 45px)" },
}} }}
language={'json'} language={"json"}
id={header} id={header}
height="100%" height="100%"
value={editorValue} value={editorValue}
@@ -181,29 +197,36 @@ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, s
glyphMargin: true, glyphMargin: true,
tabSize: editorSettings.tabSize, tabSize: editorSettings.tabSize,
dragAndDrop: true, dragAndDrop: true,
fontSize: 14 fontSize: 14,
}) });
setMonacoInst(monaco) setMonacoInst(monaco);
// register onExit cb // register onExit cb
const model = editor.getModel() const model = editor.getModel();
model?.onWillDispose(() => onExit(model.getValue())) model?.onWillDispose(() => onExit(model.getValue()));
}} }}
overlay={ overlay={
hasUnsaved ? ( hasUnsaved ? (
<Flex row align="center" css={{ fontSize: '$xs', color: '$textMuted', ml: 'auto' }}> <Flex
row
align="center"
css={{ fontSize: "$xs", color: "$textMuted", ml: "auto" }}
>
<Text muted small> <Text muted small>
This file has unsaved changes. This file has unsaved changes.
</Text> </Text>
<Link css={{ ml: '$1' }} onClick={() => saveState(editorValue || '', currTxType)}> <Link
css={{ ml: "$1" }}
onClick={() => saveState(editorValue || "", currTxType)}
>
save save
</Link> </Link>
<Link css={{ ml: '$1' }} onClick={discardChanges}> <Link css={{ ml: "$1" }} onClick={discardChanges}>
discard discard
</Link> </Link>
</Flex> </Flex>
) : undefined ) : undefined
} }
/> />
) );
} };

View File

@@ -1,158 +1,168 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { FC, 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,
transactionsOptions, transactionsOptions,
TxFields, TxFields,
getTxFields, getTxFields,
defaultTransactionType defaultTransactionType,
} from '../../state/transactions' } from "../../state/transactions";
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio";
import state from '../../state' import state from "../../state";
import { streamState } from '../DebugStream' import { streamState } from "../DebugStream";
import { Button } from '..' import { Button } from "..";
import Textarea from '../Textarea' import Textarea from "../Textarea";
interface UIProps { interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined setState: (
state: TransactionState pTx?: Partial<TransactionState> | undefined
estimateFee?: (...arg: any) => Promise<string | undefined> ) => TransactionState | undefined;
state: TransactionState;
estimateFee?: (...arg: any) => Promise<string | undefined>;
} }
export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) => { export const TxUI: FC<UIProps> = ({
const { accounts } = useSnapshot(state) state: txState,
const { selectedAccount, selectedDestAccount, selectedTransaction, txFields } = txState setState,
estimateFee,
}) => {
const { accounts } = useSnapshot(state);
const {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
} = txState;
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 [feeLoading, setFeeLoading] = useState(false);
const resetFields = useCallback( const resetFields = useCallback(
(tt: string) => { (tt: string) => {
const fields = getTxFields(tt) const fields = getTxFields(tt);
if (fields.Destination !== undefined) { if (fields.Destination !== undefined) {
setState({ selectedDestAccount: null }) fields.Destination = "";
fields.Destination = ''
} else { } else {
fields.Destination = undefined fields.Destination = undefined;
} }
return setState({ txFields: fields }) return setState({ txFields: fields, selectedDestAccount: null });
}, },
[setState] [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 = useCallback( const handleChangeTxType = useCallback(
(tt: SelectOption) => { (tt: SelectOption) => {
setState({ selectedTransaction: tt }) setState({ selectedTransaction: tt });
const newState = resetFields(tt.value) const newState = resetFields(tt.value);
handleEstimateFee(newState, true) handleEstimateFee(newState, true);
}, },
[handleEstimateFee, resetFields, setState] [handleEstimateFee, resetFields, setState]
) );
const switchToJson = () => setState({ viewType: 'json' }) const switchToJson = () => setState({ viewType: "json" });
// default tx // default tx
useEffect(() => { useEffect(() => {
if (selectedTransaction?.value) return if (selectedTransaction?.value) return;
if (defaultTransactionType) { if (defaultTransactionType) {
handleChangeTxType(defaultTransactionType) handleChangeTxType(defaultTransactionType);
} }
}, [handleChangeTxType, selectedTransaction?.value]) }, [handleChangeTxType, selectedTransaction?.value]);
const fields = useMemo( const fields = useMemo(
() => getTxFields(selectedTransaction?.value), () => getTxFields(selectedTransaction?.value),
[selectedTransaction?.value] [selectedTransaction?.value]
) );
const specialFields = ['TransactionType', 'Account'] const specialFields = ["TransactionType", "Account"];
if (fields.Destination !== undefined) { if (fields.Destination !== undefined) {
specialFields.push('Destination') specialFields.push("Destination");
} }
const otherFields = Object.keys(txFields).filter(k => !specialFields.includes(k)) as [ const otherFields = Object.keys(txFields).filter(
keyof TxFields k => !specialFields.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 <Flex
row row
fluid fluid
css={{ css={{
justifyContent: 'flex-end', justifyContent: "flex-end",
alignItems: 'center', alignItems: "center",
mb: '$3', mb: "$3",
mt: '1px', mt: "1px",
pr: '1px' pr: "1px",
}} }}
> >
<Text muted css={{ mr: '$3' }}> <Text muted css={{ mr: "$3" }}>
Transaction type:{' '} Transaction type:{" "}
</Text> </Text>
<Select <Select
instanceId="transactionsType" instanceId="transactionsType"
placeholder="Select transaction type" placeholder="Select transaction type"
options={transactionsOptions} options={transactionsOptions}
hideSelectedOptions hideSelectedOptions
css={{ width: '70%' }} css={{ width: "70%" }}
value={selectedTransaction} value={selectedTransaction}
onChange={(tt: any) => handleChangeTxType(tt)} onChange={(tt: any) => handleChangeTxType(tt)}
/> />
@@ -161,19 +171,19 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
row row
fluid fluid
css={{ css={{
justifyContent: 'flex-end', justifyContent: "flex-end",
alignItems: 'center', alignItems: "center",
mb: '$3', mb: "$3",
pr: '1px' pr: "1px",
}} }}
> >
<Text muted css={{ mr: '$3' }}> <Text muted css={{ mr: "$3" }}>
Account:{' '} Account:{" "}
</Text> </Text>
<Select <Select
instanceId="from-account" instanceId="from-account"
placeholder="Select your account" placeholder="Select your account"
css={{ width: '70%' }} 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
@@ -184,19 +194,19 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
row row
fluid fluid
css={{ css={{
justifyContent: 'flex-end', justifyContent: "flex-end",
alignItems: 'center', alignItems: "center",
mb: '$3', mb: "$3",
pr: '1px' pr: "1px",
}} }}
> >
<Text muted css={{ mr: '$3' }}> <Text muted css={{ mr: "$3" }}>
Destination account:{' '} Destination account:{" "}
</Text> </Text>
<Select <Select
instanceId="to-account" instanceId="to-account"
placeholder="Select the destination account" placeholder="Select the destination account"
css={{ width: '70%' }} css={{ width: "70%" }}
options={destAccountOptions} options={destAccountOptions}
value={selectedDestAccount} value={selectedDestAccount}
isClearable isClearable
@@ -205,37 +215,39 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
</Flex> </Flex>
)} )}
{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 ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined let rows = isJson
if (rows && rows > 5) rows = 5 ? (value?.match(/\n/gm)?.length || 0) + 1
: undefined;
if (rows && rows > 5) rows = 5;
return ( return (
<Flex column key={field} css={{ mb: '$2', pr: '1px' }}> <Flex column key={field} css={{ mb: "$2", pr: "1px" }}>
<Flex <Flex
row row
fluid fluid
css={{ css={{
justifyContent: 'flex-end', justifyContent: "flex-end",
alignItems: 'center', alignItems: "center",
position: 'relative' position: "relative",
}} }}
> >
<Text muted css={{ mr: '$3' }}> <Text muted css={{ mr: "$3" }}>
{field + (isXrp ? ' (XRP)' : '')}:{' '} {field + (isXrp ? " (XRP)" : "")}:{" "}
</Text> </Text>
{isJson ? ( {isJson ? (
<Textarea <Textarea
@@ -244,44 +256,46 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
spellCheck={false} spellCheck={false}
onChange={switchToJson} onChange={switchToJson}
css={{ css={{
width: '70%', width: "70%",
flex: 'inherit', flex: "inherit",
resize: 'vertical' resize: "vertical",
}} }}
/> />
) : ( ) : (
<Input <Input
type={isFee ? 'number' : 'text'} type={isFee ? "number" : "text"}
value={value} value={value}
onChange={e => { onChange={e => {
if (isFee) { if (isFee) {
const val = e.target.value.replaceAll('.', '').replaceAll(',', '') const val = e.target.value
handleSetField(field, val) .replaceAll(".", "")
.replaceAll(",", "");
handleSetField(field, val);
} else { } else {
handleSetField(field, e.target.value) handleSetField(field, e.target.value);
} }
}} }}
onKeyPress={ onKeyPress={
isFee isFee
? e => { ? e => {
if (e.key === '.' || e.key === ',') { if (e.key === "." || e.key === ",") {
e.preventDefault() e.preventDefault();
} }
} }
: undefined : undefined
} }
css={{ css={{
width: '70%', width: "70%",
flex: 'inherit', flex: "inherit",
'-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
}
}} }}
/> />
)} )}
@@ -294,12 +308,12 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
isDisabled={txState.txIsDisabled} isDisabled={txState.txIsDisabled}
isLoading={feeLoading} isLoading={feeLoading}
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={() => handleEstimateFee()} onClick={() => handleEstimateFee()}
> >
@@ -308,9 +322,9 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
)} )}
</Flex> </Flex>
</Flex> </Flex>
) );
})} })}
</Flex> </Flex>
</Container> </Container>
) );
} };

View File

@@ -1,5 +1,11 @@
const Carbon = () => ( const Carbon = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <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"
@@ -29,6 +35,6 @@ const Carbon = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
) );
export default Carbon export default Carbon;

View File

@@ -1,5 +1,11 @@
const Firewall = () => ( const Firewall = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <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"
@@ -64,6 +70,6 @@ const Firewall = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
) );
export default Firewall export default Firewall;

View File

@@ -1,5 +1,11 @@
const Notary = () => ( const Notary = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <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"
@@ -29,6 +35,6 @@ const Notary = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
) );
export default Notary export default Notary;

View File

@@ -1,5 +1,11 @@
const Peggy = () => ( const Peggy = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <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"
@@ -50,6 +56,6 @@ const Peggy = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
) );
export default Peggy export default Peggy;

View File

@@ -1,5 +1,11 @@
const Starter = () => ( const Starter = () => (
<svg width="66" height="32" viewBox="0 0 66 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <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"
@@ -29,6 +35,6 @@ const Starter = () => (
fill="#EDEDEF" fill="#EDEDEF"
/> />
</svg> </svg>
) );
export default Starter export default Starter;

View File

@@ -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";

View File

@@ -5,7 +5,10 @@
"schema": { "schema": {
"anyOf": [ "anyOf": [
{ {
"type": ["number", "string"], "type": [
"number",
"string"
],
"exclusiveMinimum": 0, "exclusiveMinimum": 0,
"maximum": "100000000000000000" "maximum": "100000000000000000"
}, },
@@ -16,7 +19,10 @@
"description": "Arbitrary currency code for the token. Cannot be XRP." "description": "Arbitrary currency code for the token. Cannot be XRP."
}, },
"value": { "value": {
"type": ["string", "number"], "type": [
"string",
"number"
],
"description": "Quoted decimal representation of the amount of the token." "description": "Quoted decimal representation of the amount of the token."
}, },
"issuer": { "issuer": {
@@ -41,4 +47,4 @@
} }
] ]
} }
} }

View File

@@ -109,7 +109,7 @@
"Fee": "10", "Fee": "10",
"NFTokenOffers": { "NFTokenOffers": {
"$type": "json", "$type": "json",
"$value": ["4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"] "$value": ["4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"]
} }
}, },
{ {
@@ -244,4 +244,4 @@
}, },
"Sequence": 12 "Sequence": 12
} }
] ]

View File

@@ -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;

View File

@@ -2,27 +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.experiments = {
topLevelAwait: true,
layers: true
}
if (!isServer) { if (!isServer) {
config.resolve.fallback = { config.resolve.fallback.fs = false;
...config.resolve.fallback,
fs: false,
module: false
}
} }
config.module.rules.push({ config.module.rules.push({
test: /\.md$/, test: /\.md$/,
use: 'raw-loader' use: "raw-loader",
}) });
return config return config;
} },
} };

View File

@@ -7,7 +7,6 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"format": "prettier --write .",
"postinstall": "patch-package" "postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {
@@ -26,7 +25,6 @@
"@radix-ui/react-switch": "^0.1.5", "@radix-ui/react-switch": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7", "@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.8", "@stitches/react": "^1.2.8",
"assemblyscript": "^0.20.19",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"comment-parser": "^1.3.1", "comment-parser": "^1.3.1",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
@@ -38,7 +36,7 @@
"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.10.3", "next-auth": "^4.10.1",
"next-plausible": "^3.2.0", "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",
@@ -47,14 +45,12 @@
"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",
@@ -84,4 +80,4 @@
"resolutions": { "resolutions": {
"ripple-binary-codec": "=1.4.2" "ripple-binary-codec": "=1.4.2"
} }
} }

View File

@@ -1,54 +1,60 @@
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 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 { Button, Flex } from "../components";
import { ChatCircleText } from 'phosphor-react' 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('/develop') && 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 (
<> <>
@@ -79,15 +85,37 @@ 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 rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> rel="apple-touch-icon"
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" /> <link rel="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 name="theme-color" content="#161618" media="(prefers-color-scheme: dark)" /> <meta
<meta name="theme-color" content="#FDFCFD" media="(prefers-color-scheme: light)" /> name="theme-color"
content="#161618"
media="(prefers-color-scheme: dark)"
/>
<meta
name="theme-color"
content="#FDFCFD"
media="(prefers-color-scheme: light)"
/>
</Head> </Head>
<IdProvider> <IdProvider>
@@ -97,25 +125,28 @@ 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,
}} }}
> >
<PlausibleProvider domain="hooks-builder.xrpl.org" trackOutboundLinks> <PlausibleProvider
domain="hooks-builder.xrpl.org"
trackOutboundLinks
>
<Navigation /> <Navigation />
<Component {...pageProps} /> <Component {...pageProps} />
<Toaster <Toaster
toastOptions={{ toastOptions={{
className: css({ className: css({
backgroundColor: '$mauve1', backgroundColor: "$mauve1",
color: '$mauve10', color: "$mauve10",
fontSize: '$sm', fontSize: "$sm",
zIndex: 9999, zIndex: 9999,
'.dark &': { ".dark &": {
backgroundColor: '$mauve4', backgroundColor: "$mauve4",
color: '$mauve12' color: "$mauve12",
} },
})() })(),
}} }}
/> />
<Alert /> <Alert />
@@ -124,10 +155,10 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
href="https://github.com/XRPLF/Hooks/discussions" href="https://github.com/XRPLF/Hooks/discussions"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
css={{ position: 'fixed', right: '$4', bottom: '$4' }} css={{ position: "fixed", right: "$4", bottom: "$4" }}
> >
<Button size="sm" variant="primary" outline> <Button size="sm" variant="primary" outline>
<ChatCircleText size={14} style={{ marginRight: '0px' }} /> <ChatCircleText size={14} style={{ marginRight: "0px" }} />
Bugs & Discussions Bugs & Discussions
</Button> </Button>
</Flex> </Flex>
@@ -136,6 +167,6 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
</SessionProvider> </SessionProvider>
</IdProvider> </IdProvider>
</> </>
) );
} }
export default MyApp export default MyApp;

View File

@@ -1,22 +1,35 @@
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document' import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from "next/document";
import { globalStyles, getCssText } from '../stitches.config' import { globalStyles, getCssText } from "../stitches.config";
class MyDocument extends Document { 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 id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} /> <style
id="stitches"
dangerouslySetInnerHTML={{ __html: getCssText() }}
/>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" /> <link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link <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"
@@ -27,8 +40,8 @@ class MyDocument extends Document {
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
) );
} }
} }
export default MyDocument export default MyDocument;

View File

@@ -1,10 +1,12 @@
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 === '/') {
const url = req.nextUrl.clone() if (req.nextUrl.pathname === "/") {
url.pathname = '/develop' const url = req.nextUrl.clone();
return Response.redirect(url) url.pathname = '/develop';
return Response.redirect(url);
} }
} }

View File

@@ -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,38 +10,39 @@ 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
} }
} },
}) })

View File

@@ -6,13 +6,14 @@ 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>
@@ -20,25 +21,20 @@ 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']) const ip = Array.isArray(req?.headers?.["x-real-ip"]) ? req?.headers?.["x-real-ip"][0] : req?.headers?.["x-real-ip"];
? req?.headers?.['x-real-ip'][0]
: req?.headers?.['x-real-ip']
try { try {
const response = await fetch( const response = await fetch(`https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`, {
`https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`, method: 'POST',
{ headers: {
method: 'POST', 'x-forwarded-for': ip || '',
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' })

View File

@@ -5,6 +5,9 @@ type Data = {
name: string name: string
} }
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) { export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' }) res.status(200).json({ name: 'John Doe' })
} }

View File

@@ -1,15 +1,18 @@
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(
try { req: NextApiRequest,
const { url, opts } = req.body res: NextApiResponse
const r = await fetch(url, opts) ) {
if (!r.ok) throw r.statusText try {
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!" })
} }
} }

View File

@@ -1,59 +1,63 @@
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 title="Deploy Log" logs={deployLogs} clearLog={() => (state.deployLogs = [])} /> <LogBox
title="Deploy Log"
logs={deployLogs}
clearLog={() => (state.deployLogs = [])}
/>
</div> </div>
</Split> </Split>
</Split> </Split>
) );
} };
export default Deploy export default Deploy;

View File

@@ -1,33 +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 { FileJs, 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 Popover from '../../components/Popover' import Popover from "../../components/Popover";
import RunScript from '../../components/RunScript' import RunScript from "../../components/RunScript";
import state 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";
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 no optimization: Specify which optimization level to use for compiling. For example -O0 means
this level compiles the fastest and generates the most debuggable code. -O2 means moderate level no optimization: this level compiles the fastest and generates the most
of optimization which enables most optimizations. Read more about the options from{' '} debuggable code. -O2 means moderate level of optimization which enables most
optimizations. Read more about the options from{" "}
<a <a
className="link" className="link"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -38,142 +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);
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() === 'c' || {snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
snap.files[snap.active]?.name?.split('.')?.[1]?.toLowerCase() === 'ts') && ( "c" && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)} onKeyDown={() =>
!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
@@ -186,30 +189,31 @@ const Home: NextPage = () => {
<Play weight="bold" size="16px" /> <Play weight="bold" size="16px" />
Compile to Wasm Compile to Wasm
</Button> </Button>
{snap.files[snap.active].language === 'c' && ( <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() === 'js' && ( {snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"js" && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)} onKeyDown={() =>
!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={snap.files[snap.active]} />
@@ -217,21 +221,26 @@ const Home: NextPage = () => {
</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 title="Development Log" clearLog={() => (state.logs = [])} logs={snap.logs} /> <LogBox
title="Development Log"
clearLog={() => (state.logs = [])}
logs={snap.logs}
/>
</Flex> </Flex>
{snap.files[snap.active]?.name?.split('.')?.[1]?.toLowerCase() === 'js' && ( {snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"js" && (
<Flex <Flex
css={{ css={{
flex: 1 flex: 1,
}} }}
> >
<LogBox <LogBox
@@ -244,7 +253,7 @@ const Home: NextPage = () => {
)} )}
</Flex> </Flex>
</Split> </Split>
) );
} };
export default Home export default Home;

View File

@@ -1,5 +1,5 @@
const Home = () => { const Home = () => {
return <p>homepage</p> return <p>homepage</p>;
} };
export default Home export default Home;

View File

@@ -1,37 +1,38 @@
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) void signIn('github', { redirect: false }) if (status !== "loading" && !session)
if (status !== 'loading' && session) window.close() void signIn("github", { redirect: false });
}, [session, status]) if (status !== "loading" && session) window.close();
}, [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;

View File

@@ -1,56 +1,58 @@
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, { renameTxState } from '../../state' import state, { renameTxState } from "../../state";
import { getSplit, saveSplit } from '../../state/actions/persistSplits' import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTxState } from '../../state' import { transactionsState, modifyTxState } from "../../state";
import { useEffect, useState } from 'react' import { useEffect, useState } from "react";
import { FileJs } from 'phosphor-react' import { FileJs } from "phosphor-react";
import RunScript from '../../components/RunScript' 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 = Boolean(snap.files.filter(f => f.name.toLowerCase()?.endsWith('.js')).length) const hasScripts = Boolean(
snap.files.filter(f => f.name.toLowerCase()?.endsWith(".js")).length
);
const renderNav = () => ( const renderNav = () => (
<Flex css={{ gap: '$3' }}> <Flex css={{ gap: "$3" }}>
{snap.files {snap.files
.filter(f => f.name.endsWith('.js')) .filter(f => f.name.endsWith(".js"))
.map(file => ( .map(file => (
<RunScript file={file} key={file.name} /> <RunScript file={file} key={file.name} />
))} ))}
</Flex> </Flex>
) );
return ( return (
<Container css={{ px: 0 }}> <Container css={{ px: 0 }}>
<Split <Split
direction="vertical" direction="vertical"
sizes={ sizes={
hasScripts && getSplit('testVertical')?.length === 2 hasScripts && getSplit("testVertical")?.length === 2
? [50, 20, 30] ? [50, 20, 30]
: hasScripts : hasScripts
? [50, 20, 50] ? [50, 20, 50]
@@ -58,45 +60,49 @@ const Test = () => {
} }
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" 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
defaultExtension="json" defaultExtension="json"
allowedExtensions={['json']} allowedExtensions={["json"]}
onCreateNewTab={header => modifyTxState(header, {})} onCreateNewTab={header => modifyTxState(header, {})}
onRenameTab={(idx, nwName, oldName = '') => renameTxState(oldName, nwName)} onRenameTab={(idx, nwName, oldName = "") =>
onCloseTab={(idx, header) => header && modifyTxState(header, undefined)} renameTxState(oldName, nwName)
}
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}>
@@ -105,7 +111,7 @@ 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>
@@ -114,9 +120,9 @@ const Test = () => {
<Flex <Flex
as="div" as="div"
css={{ css={{
borderTop: '1px solid $mauve6', borderTop: "1px solid $mauve6",
background: '$mauve1', background: "$mauve1",
flexDirection: 'column' flexDirection: "column",
}} }}
> >
<LogBox <LogBox
@@ -136,16 +142,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
@@ -154,14 +160,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;

View File

@@ -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"
} }

8
raw-loader.d.ts vendored
View File

@@ -1,4 +1,4 @@
declare module '*.md' { declare module "*.md" {
const content: string const content: string;
export default content export default content;
} };

View File

@@ -1,24 +1,25 @@
import toast from 'react-hot-toast'
import state, { FaucetAccountRes } from '../index' import toast from "react-hot-toast";
import state, { FaucetAccountRes } from '../index';
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
@@ -29,22 +30,22 @@ export const names = [
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => { export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
if (typeof window === undefined) return if (typeof window === undefined) return
const toastId = showToast ? toast.loading('Creating account') : '' const toastId = showToast ? toast.loading("Creating account") : "";
const res = await fetch(`${window.location.origin}/api/faucet`, { const res = await fetch(`${window.location.origin}/api/faucet`, {
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) {
if (showToast) { if (showToast) {
return toast.error(json.error, { id: toastId }) return toast.error(json.error, { id: toastId });
} else { } else {
return return;
} }
} else { } else {
if (showToast) { if (showToast) {
toast.success('New account created', { id: toastId }) toast.success("New account created", { id: toastId });
} }
const currNames = state.accounts.map(acc => acc.name) const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({ state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0], name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(), xrp: (json.xrp || 0 * 1000000).toString(),
@@ -54,35 +55,36 @@ export const addFaucetAccount = async (name?: string, showToast: boolean = false
hooks: [], hooks: [],
isLoading: false, isLoading: false,
version: '2' version: '2'
}) });
} }
} };
// fetch initial faucets // fetch initial faucets
;(async function fetchFaucets() { (async function fetchFaucets() {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
if (state.accounts.length === 0) { if (state.accounts.length === 0) {
await addFaucetAccount() await addFaucetAccount();
// setTimeout(() => { // setTimeout(() => {
// addFaucetAccount(); // addFaucetAccount();
// }, 10000); // }, 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();
} }
} }
}
}

View File

@@ -1,110 +1,30 @@
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) => {
let asc: typeof import('assemblyscript/dist/asc') | undefined
// Save the file to global state // Save the file to global state
saveFile(false, activeId) saveFile(false, 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 // TODO Inform user about it. // 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;
if (typeof window !== 'undefined') {
// IF AssemblyScript
if (
state.files[activeId].language.toLowerCase() === 'ts' ||
state.files[activeId].language.toLowerCase() === 'typescript'
) {
if (!asc) {
asc = await import('assemblyscript/dist/asc')
}
const files: { [key: string]: string } = {}
state.files.forEach(file => {
files[file.name] = file.content
})
const res = await asc.main(
[
state.files[activeId].name,
'--textFile',
'-o',
state.files[activeId].name,
'--runtime',
'minimal',
'-O3'
],
{
readFile: (name, baseDir) => {
console.log('--> ', name)
const currentFile = state.files.find(file => file.name === name)
if (currentFile) {
return currentFile.content
}
return null
},
writeFile: (name, data, baseDir) => {
console.log(name)
const curr = state.files.find(file => file.name === name)
if (curr) {
curr.compiledContent = ref(data)
}
},
listFiles: (dirname, baseDir) => {
console.log('listFiles: ' + dirname + ', baseDir=' + baseDir)
return []
}
}
)
// In case you want to compile just single file
// const res = await asc.compileString(state.files[activeId].content, {
// optimizeLevel: 3,
// runtime: 'stub',
// })
if (res.error?.message) {
state.compiling = false
state.logs.push({
type: 'error',
message: res.error.message
})
state.logs.push({
type: 'error',
message: res.stderr.toString()
})
return
}
if (res.stdout) {
const wat = res.stdout.toString()
state.files[activeId].lastCompiled = new Date()
state.files[activeId].compiledWatContent = wat
state.files[activeId].compiledValueSnapshot = state.files[activeId].content
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'
})
}
state.compiling = false
return
}
}
state.logs = [] state.logs = []
const file = state.files[activeId] const file = state.files[activeId]
try { try {
@@ -112,29 +32,29 @@ export const compileCode = async (activeId: number) => {
let res: Response let res: Response
try { try {
res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, { res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
output: 'wasm', output: "wasm",
compress: true, compress: true,
strip: state.compileOptions.strip, strip: state.compileOptions.strip,
files: [ files: [
{ {
type: 'c', type: "c",
options: state.compileOptions.optimizationLevel || '-O2', options: state.compileOptions.optimizationLevel || '-O2',
name: file.name, name: file.name,
src: file.content src: file.content,
} },
] ],
}) }),
}) });
} catch (error) { } catch (error) {
throw Error('Something went wrong, check your network connection and try again!') throw Error("Something went wrong, check your network connection and try again!")
} }
const json = await res.json() const json = await res.json();
state.compiling = false state.compiling = false;
if (!json.success) { if (!json.success) {
const errors = [json.message] const errors = [json.message]
if (json.tasks && json.tasks.length > 0) { if (json.tasks && json.tasks.length > 0) {
@@ -142,63 +62,65 @@ export const compileCode = async (activeId: number) => {
if (!task.success) { if (!task.success) {
errors.push(task?.console) errors.push(task?.console)
} }
}) });
} }
throw errors throw errors
} }
try { try {
// Decode base64 encoded wasm that is coming back from the endpoint // Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output) const bufferData = await decodeBinary(json.output);
// Import wabt from and create human readable version of wasm file and // Import wabt from and create human readable version of wasm file and
// put it into state // put it into state
const ww = (await import('wabt')).default() const ww = (await import('wabt')).default()
const myModule = ww.readWasm(new Uint8Array(bufferData), { const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true readDebugNames: true,
}) });
myModule.applyNames() myModule.applyNames();
const wast = myModule.toText({ foldExprs: false, inlineExport: false }) const wast = myModule.toText({ foldExprs: false, inlineExport: false });
file.compiledContent = ref(bufferData) file.compiledContent = ref(bufferData);
file.lastCompiled = new Date() file.lastCompiled = new Date();
file.compiledValueSnapshot = file.content file.compiledValueSnapshot = file.content
file.compiledWatContent = wast file.compiledWatContent = wast;
} catch (error) { } catch (error) {
throw Error('Invalid compilation result produced, check your code for errors and try again!') throw Error("Invalid compilation result produced, check your code for errors and try again!")
} }
toast.success('Compiled successfully!', { position: 'bottom-center' }) toast.success("Compiled successfully!", { position: "bottom-center" });
state.logs.push({ state.logs.push({
type: 'success', type: "success",
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`, message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace('develop', 'deploy'), link: Router.asPath.replace("develop", "deploy"),
linkText: 'Go to deploy' linkText: "Go to deploy",
}) });
} catch (err) { } catch (err) {
console.log(err) console.log(err);
if (err instanceof Array && typeof err[0] === 'string') { if (err instanceof Array && typeof err[0] === 'string') {
err.forEach(message => { err.forEach(message => {
state.logs.push({ state.logs.push({
type: 'error', type: "error",
message 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!'
}) })
} }
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 state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' }) toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
file.containsErrors = true file.containsErrors = true
} }
} };

View File

@@ -1,28 +1,24 @@
import state, { IFile } from '../index' import state, { IFile } from '../index';
const languageMapping = { const languageMapping = {
ts: 'typescript', 'ts': 'typescript',
js: 'javascript', 'js': 'javascript',
md: 'markdown', 'md': 'markdown',
c: 'c', 'c': 'c',
h: 'c', 'h': 'c',
other: '' 'other': ''
} /* Initializes empty file to global state */ } /* Initializes empty file to global state */
export const createNewFile = (name: string) => { export const createNewFile = (name: string) => {
const tempName = name.split('.') const tempName = name.split('.');
const fileExt = tempName[tempName.length - 1] || 'other' const fileExt = tempName[tempName.length - 1] || 'other';
const emptyFile: IFile = { const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
name, state.files.push(emptyFile);
language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], state.active = state.files.length - 1;
content: '' };
}
state.files.push(emptyFile)
state.active = state.files.length - 1
}
export const renameFile = (oldName: string, nwName: string) => { export const renameFile = (oldName: string, nwName: string) => {
const file = state.files.find(file => file.name === oldName) const file = state.files.find(file => file.name === oldName)
if (!file) throw Error(`No file exists with name ${oldName}`) if (!file) throw Error(`No file exists with name ${oldName}`)
file.name = nwName file.name = nwName
} };

View File

@@ -1,24 +0,0 @@
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
})
}

View File

@@ -1,51 +1,53 @@
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 { 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 } from '../../utils/setHook' import { SetHookData } from '../../utils/setHook';
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.map(bytes => bytes.toString(16).padStart(2, '0')).join('') const hashHex = hashArray
return hashHex .map((bytes) => bytes.toString(16).padStart(2, "0"))
} .join("");
return hashHex;
};
function toHex(str: string) { function toHex(str: string) {
var result = '' var result = "";
for (var i = 0; i < str.length; i++) { for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16) result += str.charCodeAt(i).toString(16);
} }
return result.toUpperCase() return result.toUpperCase();
} }
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | Uint8Array | 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 = arrayBuffer instanceof Uint8Array ? arrayBuffer : 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 (
@@ -54,29 +56,30 @@ export const prepareDeployHookTx = async (
) => { ) => {
const activeFile = state.files[state.active]?.compiledContent const activeFile = state.files[state.active]?.compiledContent
? state.files[state.active] ? state.files[state.active]
: state.files.filter(file => file.compiledContent)[0] : state.files.filter((file) => file.compiledContent)[0];
if (!state.files || state.files.length === 0) { if (!state.files || state.files.length === 0) {
return return;
} }
if (!activeFile?.compiledContent) { if (!activeFile?.compiledContent) {
return return;
} }
if (!state.client) { if (!state.client) {
return return;
} }
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase() const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase();
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value) const hookOnValues: (keyof TTS)[] = data.Invoke.map((tt) => tt.value);
const { HookParameters } = data const { HookParameters } = data;
const filteredHookParameters = HookParameters.filter( const filteredHookParameters = HookParameters.filter(
hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue (hp) =>
)?.map(aa => ({ hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
)?.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: {
@@ -86,74 +89,82 @@ export const prepareDeployHookTx = async (
// } // }
// } // }
// }); // });
if (typeof window !== 'undefined') { 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: data.Fee, Fee: data.Fee,
Hooks: [ Hooks: [
{ {
Hook: { Hook: {
CreateCode: arrayBufferToHex(activeFile?.compiledContent).toUpperCase(), CreateCode: arrayBufferToHex(
activeFile?.compiledContent
).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 return tx;
} }
} };
/* deployHook function turns the wasm binary into /* deployHook function turns the wasm binary into
* hex string, signs the transaction and deploys it to * hex string, signs the transaction and deploys it to
* Hooks testnet. * Hooks testnet.
*/ */
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => { export const deployHook = async (
if (typeof window !== 'undefined') { account: IAccount & { name?: string },
data: SetHookData
) => {
if (typeof window !== "undefined") {
const activeFile = state.files[state.active]?.compiledContent const activeFile = state.files[state.active]?.compiledContent
? state.files[state.active] ? state.files[state.active]
: state.files.filter(file => file.compiledContent)[0] : state.files.filter((file) => file.compiledContent)[0];
state.deployValues[activeFile.name] = data state.deployValues[activeFile.name] = data;
const tx = await prepareDeployHookTx(account, data) const tx = await prepareDeployHookTx(account, data);
if (!tx) { if (!tx) {
return return;
} }
if (!state.client) { if (!state.client) {
return return;
} }
const keypair = derive.familySeed(account.secret) const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair) const { signedTransaction } = sign(tx, keypair);
const currentAccount = state.accounts.find(acc => acc.address === account.address) const currentAccount = state.accounts.find(
(acc) => acc.address === account.address
);
if (currentAccount) { if (currentAccount) {
currentAccount.isLoading = true currentAccount.isLoading = true;
} }
let submitRes let submitRes;
try { try {
submitRes = await state.client?.send({ submitRes = await state.client?.send({
command: 'submit', command: "submit",
tx_blob: signedTransaction tx_blob: signedTransaction,
}) });
if (submitRes.engine_result === 'tesSUCCESS') { if (submitRes.engine_result === "tesSUCCESS") {
state.deployLogs.push({ state.deployLogs.push({
type: 'success', type: "success",
message: 'Hook deployed successfully ✅' message: "Hook deployed successfully ✅",
}) });
state.deployLogs.push({ state.deployLogs.push({
type: 'success', type: "success",
message: ref( message: ref(
<> <>
[{submitRes.engine_result}] {submitRes.engine_result_message} Transaction hash:{' '} [{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
Transaction hash:{" "}
<Link <Link
as="a" as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.tx_json?.hash}`} href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.tx_json?.hash}`}
@@ -163,110 +174,113 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
{submitRes.tx_json?.hash} {submitRes.tx_json?.hash}
</Link> </Link>
</> </>
) ),
// message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`, // message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
}) });
} else { } else {
state.deployLogs.push({ state.deployLogs.push({
type: 'error', type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${ message: `[${submitRes.engine_result || submitRes.error}] ${
submitRes.engine_result_message || submitRes.error_exception submitRes.engine_result_message || submitRes.error_exception
}` }`,
}) });
} }
} catch (err) { } catch (err) {
console.log(err) console.log(err);
state.deployLogs.push({ state.deployLogs.push({
type: 'error', type: "error",
message: 'Error occurred while deploying' message: "Error occurred while deploying",
}) });
} }
if (currentAccount) { if (currentAccount) {
currentAccount.isLoading = false currentAccount.isLoading = false;
} }
return submitRes return submitRes;
} }
} };
export const deleteHook = async (account: IAccount & { name?: string }) => { export const deleteHook = async (account: IAccount & { name?: string }) => {
if (!state.client) { if (!state.client) {
return return;
} }
const currentAccount = state.accounts.find(acc => acc.address === account.address) 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') { 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",
Hooks: [ Hooks: [
{ {
Hook: { Hook: {
CreateCode: '', CreateCode: "",
Flags: 1 Flags: 1,
} },
} },
] ],
} };
const keypair = derive.familySeed(account.secret) const keypair = derive.familySeed(account.secret);
try { try {
// Update tx Fee value with network estimation // Update tx Fee value with network estimation
const res = await estimateFee(tx, account) const res = await estimateFee(tx, account);
tx['Fee'] = res?.base_fee ? res?.base_fee : '1000' tx["Fee"] = res?.base_fee ? res?.base_fee : "1000";
} catch (err) { } catch (err) {
// use default value what you defined earlier // use default value what you defined earlier
console.log(err) console.log(err);
} }
const { signedTransaction } = sign(tx, keypair) const { signedTransaction } = sign(tx, keypair);
if (currentAccount) { if (currentAccount) {
currentAccount.isLoading = true currentAccount.isLoading = true;
} }
let submitRes let submitRes;
const toastId = toast.loading('Deleting hook...') const toastId = toast.loading("Deleting hook...");
try { try {
submitRes = await state.client.send({ submitRes = await state.client.send({
command: 'submit', command: "submit",
tx_blob: signedTransaction tx_blob: signedTransaction,
}) });
if (submitRes.engine_result === 'tesSUCCESS') { if (submitRes.engine_result === "tesSUCCESS") {
toast.success('Hook deleted successfully ✅', { id: toastId }) toast.success("Hook deleted successfully ✅", { id: toastId });
state.deployLogs.push({ state.deployLogs.push({
type: 'success', type: "success",
message: 'Hook deleted successfully ✅' message: "Hook deleted successfully ✅",
}) });
state.deployLogs.push({ state.deployLogs.push({
type: 'success', type: "success",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}` message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
}) });
currentAccount.hooks = [] currentAccount.hooks = [];
} else { } else {
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, { toast.error(
id: toastId `${submitRes.engine_result_message || submitRes.error_exception}`,
}) { id: toastId }
);
state.deployLogs.push({ state.deployLogs.push({
type: 'error', type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${ message: `[${submitRes.engine_result || submitRes.error}] ${
submitRes.engine_result_message || submitRes.error_exception submitRes.engine_result_message || submitRes.error_exception
}` }`,
}) });
} }
} catch (err) { } catch (err) {
console.log(err) console.log(err);
toast.error('Error occurred while deleting hook', { id: toastId }) toast.error("Error occurred while deleting hook", { id: toastId });
state.deployLogs.push({ state.deployLogs.push({
type: 'error', type: "error",
message: 'Error occurred while deleting hook' message: "Error occurred while deleting hook",
}) });
} }
if (currentAccount) { if (currentAccount) {
currentAccount.isLoading = false currentAccount.isLoading = false;
} }
return submitRes return submitRes;
} }
} };

View File

@@ -1,22 +1,20 @@
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 const wasmFiles = state.files.filter(i => i.compiledContent).map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent }));
.filter(i => i.compiledContent) const zipped = await createZip([...files, ...wasmFiles]);
.map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent })) const zipFileName = guessZipFileName(files);
const zipped = await createZip([...files, ...wasmFiles]) zipped.saveFile(zipFileName);
const zipFileName = guessZipFileName(files) } catch (error) {
zipped.saveFile(zipFileName) toast.error('Error occurred while creating zip file, try again later')
} catch (error) { } finally {
toast.error('Error occurred while creating zip file, try again later') state.zipLoading = false
} finally { }
state.zipLoading = false };
}
}

View File

@@ -1,99 +1,117 @@
import { Octokit } from '@octokit/core' import { Octokit } from "@octokit/core";
import state, { IFile } from '../index' import Router from "next/router";
import { templateFileIds } from '../constants' import state from '../index';
import { templateFileIds } from '../constants';
const octokit = new Octokit() const octokit = new Octokit();
/** /* Fetches Gist files from Githug Gists based on
* Fetches files from Github Gists based on gistId and stores them in global state * gistId and stores the content in global state
*/ */
export const fetchFiles = async (gistId: string) => { export const fetchFiles = (gistId: string) => {
if (!gistId || state.files.length) return state.loading = true;
if (gistId && !state.files.length) {
state.logs.push({
type: "log",
message: `Fetching Gist with id: ${gistId}`,
});
state.loading = true octokit
state.logs.push({ .request("GET /gists/{gist_id}", { gist_id: gistId })
type: 'log', .then(async res => {
message: `Fetching Gist with id: ${gistId}` if (!Object.values(templateFileIds).map(v => v.id).includes(gistId)) {
}) return res
try { }
const res = await octokit.request('GET /gists/{gist_id}', { gist_id: gistId }) // in case of templates, fetch header file(s) and append to res
try {
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
if (resHeader.ok) {
const resHeaderJson = await resHeader.json()
const headerFiles: Record<string, { filename: string; content: string; language: string }> = {};
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)
}
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() return res;
const headerFiles: Record<string, { filename: string; content: string; language: string }> = // 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 } }) => {
Object.entries(headerJson).forEach(([key, value]) => { // const files = { ...res.data.files, ...headerFiles }
const fname = `${key}.h` // console.log(headerFiles)
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' } // res.data.files = files
// return res
// })
}) })
const files = { .then((res) => {
...res.data.files, if (res.data.files && Object.keys(res.data.files).length > 0) {
...headerFiles const files = Object.keys(res.data.files).map((filename) => ({
} name: res.data.files?.[filename]?.filename || "untitled.c",
res.data.files = files language: res.data.files?.[filename]?.language?.toLowerCase() || "",
} content: res.data.files?.[filename]?.content || "",
}));
if (!res.data.files) throw Error('No files could be fetched from given gist id!') // Sort files so that the source files are first
// In case of other files leave the order as it its
const files: IFile[] = Object.keys(res.data.files).map(filename => ({ files.sort((a, b) => {
name: res.data.files?.[filename]?.filename || 'untitled.c', const aBasename = a.name.split('.')?.[0];
language: res.data.files?.[filename]?.language?.toLowerCase() || '', const aCext = a.name?.toLowerCase().endsWith('.c');
content: res.data.files?.[filename]?.content || '' 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
files.sort((a, b) => { if (aCext && !bCext) {
const aBasename = a.name.split('.')?.[0] return -1;
const aExt = a.name.split('.').pop() || '' }
const bBasename = b.name.split('.')?.[0] if (!aCext && bCext) {
const bExt = b.name.split('.').pop() || '' return 1
}
// default priority is undefined == 0 // Otherwise fallback to default sorting based on basename
const extPriority: Record<string, number> = { if (aBasename > bBasename) {
c: 3, return 1;
md: 2, }
h: -1 if (bBasename > aBasename) {
} return -1;
}
// Sort based on extention priorities return 0;
const comp = (extPriority[bExt] || 0) - (extPriority[aExt] || 0) })
if (comp !== 0) return comp state.loading = false;
if (files.length > 0) {
// Otherwise fallback to alphabetical sorting state.logs.push({
return aBasename.localeCompare(bBasename) type: "success",
}) message: "Fetched successfully ✅",
});
state.logs.push({ state.files = files;
type: 'success', state.gistId = gistId;
message: 'Fetched successfully ✅' state.gistName = Object.keys(res.data.files)?.[0] || "untitled";
}) state.gistOwner = res.data.owner?.login;
state.files = files return;
state.gistId = gistId } else {
state.gistOwner = res.data.owner?.login // Open main modal if now files
state.mainModalOpen = true;
const gistName = }
files.find(file => file.language === 'c' || file.language === 'javascript')?.name || return Router.push({ pathname: "/develop" });
'untitled' }
state.gistName = gistName state.loading = false;
} catch (err) { })
console.error(err) .catch((err) => {
let message: string // console.error(err)
if (err instanceof Error) message = err.message state.loading = false;
else message = `Something went wrong, try again later!` state.logs.push({
state.logs.push({ type: "error",
type: 'error', message: `Couldn't find Gist with id: ${gistId}`,
message: `Error: ${message}` });
}) return;
});
return;
} }
state.loading = false state.loading = false;
} };

View File

@@ -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, name?: 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 occurred 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: 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!");
} };

View File

@@ -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
} };

View File

@@ -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,3 +12,4 @@ export const getSplit = (splitId: string): SplitSize | null => {
const split = splits[splitId] const split = splits[splitId]
return split ? split : null return split ? split : null
} }

View File

@@ -1,28 +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, activeId?: number) => { 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] const file = state.files[activeId || state.active]
if (state.files.length > 0) { if (state.files.length > 0) {
file.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() || '';
} }
}) })
} }

View File

@@ -1,66 +1,57 @@
import { derive, sign } from 'xrpl-accountlib' import { derive, sign } from "xrpl-accountlib";
import state from '..' import state from '..'
import type { IAccount } from '..' import type { IAccount } from "..";
interface TransactionOptions { interface TransactionOptions {
TransactionType: string TransactionType: string,
Account?: string Account?: string,
Fee?: string Fee?: string,
Destination?: string Destination?: string
[index: string]: any [index: string]: any
} }
interface OtherOptions { interface OtherOptions {
logPrefix?: string logPrefix?: string
} }
export const sendTransaction = async ( export const sendTransaction = async (account: IAccount, txOptions: TransactionOptions, options?: OtherOptions) => {
account: IAccount, if (!state.client) throw Error('XRPL client not initalized')
txOptions: TransactionOptions,
options?: OtherOptions
) => {
if (!state.client) throw Error('XRPL client not initalized')
const { Fee = '1000', ...opts } = txOptions const { Fee = "1000", ...opts } = txOptions
const tx: TransactionOptions = { const tx: TransactionOptions = {
Account: account.address, Account: account.address,
Sequence: account.sequence, Sequence: account.sequence,
Fee, // TODO auto-fillable default Fee, // TODO auto-fillable default
...opts ...opts
} };
const { logPrefix = '' } = options || {} const { logPrefix = '' } = options || {}
try { try {
const signedAccount = derive.familySeed(account.secret) const signedAccount = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, signedAccount) const { signedTransaction } = sign(tx, signedAccount);
const response = await state.client.send({ const response = await state.client.send({
command: 'submit', command: "submit",
tx_blob: signedTransaction tx_blob: signedTransaction,
}) });
if (response.engine_result === 'tesSUCCESS') { if (response.engine_result === "tesSUCCESS") {
state.transactionLogs.push({ state.transactionLogs.push({
type: 'success', type: 'success',
message: `${logPrefix}[${response.engine_result}] ${response.engine_result_message}` message: `${logPrefix}[${response.engine_result}] ${response.engine_result_message}`
}) })
} else { } else {
state.transactionLogs.push({ state.transactionLogs.push({
type: 'error', type: "error",
message: `${logPrefix}[${response.error || response.engine_result}] ${ message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
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`,
});
} }
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`
})
}
}

View File

@@ -1,27 +1,23 @@
import { ref } from 'valtio' import { ref } from 'valtio';
import { AlertState, alertState } from '../../components/AlertDialog' import { AlertState, alertState } from "../../components/AlertDialog";
export const showAlert = ( export const showAlert = (title: string, opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}) => {
title: string, const { body: _body, confirmPrefix: _confirmPrefix, ...rest } = opts
opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {} const body = (_body && typeof _body === 'object') ? ref(_body) : _body
) => { 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
}) })
} }

View File

@@ -1,97 +1,104 @@
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 (session?: Session | null, createNewGist?: boolean) => { export const syncToGist = async (
saveAllFiles() session?: Session | null,
let files: Record<string, { filename: string; content: string }> = {} createNewGist?: boolean
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 (state.gistId && session?.user.username === state.gistOwner && !createNewGist) { if (
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;

View File

@@ -1,12 +1,14 @@
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 = (editorSettings: IState['editorSettings']) => { export const updateEditorSettings = (
state.editorCtx?.getModels().forEach(model => { editorSettings: IState["editorSettings"]
) => {
state.editorCtx?.getModels().forEach((model) => {
model.updateOptions({ model.updateOptions({
...editorSettings ...editorSettings,
}) });
}) });
return (state.editorSettings = editorSettings) return (state.editorSettings = editorSettings);
} };

View File

@@ -1 +1 @@
export * from './templates' export * from './templates'

View File

@@ -1,41 +1,41 @@
import Carbon from '../../components/icons/Carbon' import Carbon from "../../components/icons/Carbon";
import Firewall from '../../components/icons/Firewall' import Firewall from "../../components/icons/Firewall";
import Notary from '../../components/icons/Notary' import Notary from "../../components/icons/Notary";
import Peggy from '../../components/icons/Peggy' import Peggy from "../../components/icons/Peggy";
import Starter from '../../components/icons/Starter' import Starter from "../../components/icons/Starter";
export const templateFileIds = { export const templateFileIds = {
starter: { 'starter': {
id: '1f8109c80f504e6326db2735df2f0ad6', // Forked id: '9106f1fe60482d90475bfe8f1315affe',
name: 'Starter', name: 'Starter',
description: description: 'Just a basic starter with essential imports, just accepts any transaction coming through',
'Just a basic starter with essential imports, just accepts any transaction coming through', icon: Starter
icon: Starter
}, },
firewall: { 'firewall': {
id: '1cc30f39c8a0b9c55b88c312669ca45e', // Forked id: '1cc30f39c8a0b9c55b88c312669ca45e', // Forked
name: 'Firewall', name: 'Firewall',
description: 'This Hook essentially checks a blacklist of accounts', description: 'This Hook essentially checks a blacklist of accounts',
icon: Firewall icon: Firewall
}, },
notary: { 'notary': {
id: '87b6f5a8c2f5038fb0f20b8b510efa10', // Forked id: '87b6f5a8c2f5038fb0f20b8b510efa10', // Forked
name: 'Notary', name: 'Notary',
description: 'Collecting signatures for multi-sign transactions', description: 'Collecting signatures for multi-sign transactions',
icon: Notary icon: Notary
}, },
carbon: { 'carbon': {
id: '953662b22d065449f8ab6f69bc2afe41', // Forked id: '5941c19dce3e147948f564e224553c02',
name: 'Carbon', name: 'Carbon',
description: 'Send a percentage of sum to an address', description: 'Send a percentage of sum to an address',
icon: Carbon icon: Carbon
}, },
peggy: { 'peggy': {
id: '049784a83fa068faf7912f663f7b6471', // Forked id: '049784a83fa068faf7912f663f7b6471', // Forked
name: 'Peggy', name: 'Peggy',
description: 'An oracle based stable coin hook', description: 'An oracle based stable coin hook',
icon: Peggy icon: Peggy
} },
} }
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'macro.h', 'extern.h', 'error.h'] export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'macro.h', 'extern.h', 'error.h'];

View File

@@ -1,92 +1,92 @@
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, subscribeKey } 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;
compiledValueSnapshot?: string compiledValueSnapshot?: string
compiledContent?: ArrayBuffer | Uint8Array | null compiledContent?: ArrayBuffer | null;
compiledWatContent?: string | null compiledWatContent?: string | null;
lastCompiled?: Date lastCompiled?: Date
containsErrors?: boolean 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 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 | null;
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 deployValues: DeployValue
} }
@@ -110,11 +110,11 @@ let initialState: IState = {
gistLoading: false, gistLoading: false,
zipLoading: false, zipLoading: false,
editorSettings: { editorSettings: {
tabSize: 2 tabSize: 2,
}, },
splits: {}, splits: {},
client: null, client: null,
clientStatus: 'offline' as 'offline', clientStatus: "offline" as "offline",
mainModalOpen: false, mainModalOpen: false,
mainModalShowed: false, mainModalShowed: false,
accounts: [], accounts: [],
@@ -123,23 +123,23 @@ let initialState: IState = {
strip: true strip: true
}, },
deployValues: {} 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');
@@ -149,35 +149,36 @@ 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}`);
client.on('online', () => { client.on("online", () => {
state.client = ref(client) 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.accounts, () => { subscribe(state.accounts, () => {
const { accounts } = 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));
}) });
const updateActiveWat = () => { const updateActiveWat = () => {
const filename = state.files[state.active]?.name const filename = state.files[state.active]?.name
const compiledFiles = state.files.filter(file => file.compiledContent) const compiledFiles = state.files.filter(
file => file.compiledContent)
const idx = compiledFiles.findIndex(file => file.name === filename) const idx = compiledFiles.findIndex(file => file.name === filename)
if (idx !== -1) state.activeWat = idx if (idx !== -1) state.activeWat = idx

View File

@@ -1,252 +1,260 @@
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';
export type SelectOption = { export type SelectOption = {
value: string value: string;
label: string label: string;
} };
export interface TransactionState { export interface TransactionState {
selectedTransaction: SelectOption | null selectedTransaction: SelectOption | null;
selectedAccount: SelectOption | null selectedAccount: SelectOption | null;
selectedDestAccount: SelectOption | null selectedDestAccount: SelectOption | null;
txIsLoading: boolean txIsLoading: boolean;
txIsDisabled: boolean txIsDisabled: boolean;
txFields: TxFields txFields: TxFields;
viewType: 'json' | 'ui' viewType: 'json' | 'ui',
editorValue?: string editorValue?: string,
estimatedFee?: string estimatedFee?: string
} }
export type TxFields = Omit< export type TxFields = Omit<
Partial<typeof transactionsData[0]>, Partial<typeof transactionsData[0]>,
'Account' | 'Sequence' | 'TransactionType' "Account" | "Sequence" | "TransactionType"
> >;
export const defaultTransaction: TransactionState = { export const defaultTransaction: TransactionState = {
selectedTransaction: null, selectedTransaction: null,
selectedAccount: null, selectedAccount: null,
selectedDestAccount: null, selectedDestAccount: null,
txIsLoading: false, txIsLoading: false,
txIsDisabled: false, txIsDisabled: false,
txFields: {}, txFields: {},
viewType: 'ui' 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) => { export const renameTxState = (oldName: string, nwName: string) => {
const tx = transactionsState.transactions.find(tx => tx.header === oldName) const tx = transactionsState.transactions.find(tx => tx.header === oldName);
if (!tx) throw Error(`No transaction state exists with given header name ${oldName}`) if (!tx) throw Error(`No transaction state exists with given header name ${oldName}`);
tx.header = nwName 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 modifyTxState = ( 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 (opts.replaceState) { if (!tx) {
const repTx: TransactionState = { const state = {
...defaultTransaction, ...defaultTransaction,
...partialTx ...partialTx,
}
transactionsState.transactions.push({
header,
state,
});
return state;
} }
tx.state = repTx
return repTx
}
Object.keys(partialTx).forEach(k => { if (opts.replaceState) {
// Typescript mess here, but is definitely safe! const repTx: TransactionState = {
const s = tx.state as any ...defaultTransaction,
const p = partialTx as any // ? Make copy ...partialTx,
if (!deepEqual(s[k], p[k])) s[k] = p[k] }
}) tx.state = repTx
return repTx
}
return tx.state Object.keys(partialTx).forEach(k => {
} // Typescript mess here, but is definitely 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 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}': ${ const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ""
error instanceof Error ? error.message : '' }`;
}` console.error(message)
console.error(message) options[field] = _value.$value
options[field] = _value.$value }
}
} }
}
}
// delete unnecessary fields // delete unnecessary fields
if (!options[field]) { 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, ...rest } = options;
let tx: Partial<TransactionState> = {} let tx: Partial<TransactionState> = {};
const schema = 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 = { tx.selectedAccount = null;
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 (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.selectedDestAccount = null tx.selectedTransaction = null;
} }
} else if (Destination) {
rest.Destination = Destination
}
Object.keys(rest).forEach(field => { if (schema.Destination !== undefined) {
const value = rest[field] const dest = state.accounts.find(acc => acc.address === Destination);
const schemaVal = schema[field as keyof TxFields] if (dest) {
const isXrp = tx.selectedDestAccount = {
typeof value !== 'object' && label: dest.name,
schemaVal && value: dest.address,
typeof schemaVal === 'object' && };
schemaVal.$type === 'xrp' }
if (isXrp) { else if (Destination) {
rest[field] = { tx.selectedDestAccount = {
$type: 'xrp', label: Destination,
$value: +value / 1000000 // ! maybe use bigint? value: Destination,
} };
} else if (typeof value === 'object') { }
rest[field] = { else {
$type: 'json', tx.selectedDestAccount = null
$value: value }
} }
else if (Destination) {
rest.Destination = Destination
} }
})
tx.txFields = rest Object.keys(rest).forEach(field => {
const value = rest[field];
const schemaVal = schema[field as keyof TxFields]
const isXrp = typeof value !== 'object' && schemaVal && typeof schemaVal === 'object' && schemaVal.$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,
};
}
});
return tx tx.txFields = rest;
return tx
} }
export const getTxFields = (tt?: string) => { export const getTxFields = (tt?: string) => {
const txFields: TxFields | undefined = transactionsData.find(tx => tx.TransactionType === tt) const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === tt
);
if (!txFields) return {} if (!txFields) return {}
let _txFields = Object.keys(txFields) let _txFields = Object.keys(txFields)
.filter(key => !['TransactionType', 'Account', 'Sequence'].includes(key)) .filter(
.reduce<TxFields>((tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf), {}) key => !["TransactionType", "Account", "Sequence"].includes(key)
return _txFields )
.reduce<TxFields>(
(tf, key) => (
(tf[key as keyof TxFields] = (txFields as any)[key]), tf
),
{}
);
return _txFields
} }
export { transactionsData } export { transactionsData }
export const transactionsOptions = transactionsData.map(tx => ({ export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType, value: tx.TransactionType,
label: tx.TransactionType label: tx.TransactionType,
})) }));
export const defaultTransactionType = transactionsOptions.find(tt => tt.value === 'Payment') export const defaultTransactionType = transactionsOptions.find(tt => tt.value === 'Payment')

View File

@@ -1,6 +1,6 @@
// stitches.config.ts // stitches.config.ts
import type Stitches from '@stitches/react' import type Stitches from "@stitches/react";
import { createStitches } from '@stitches/react' import { createStitches } from "@stitches/react";
import { import {
gray, gray,
@@ -24,285 +24,335 @@ import {
purpleDark, purpleDark,
greenDark, greenDark,
red, red,
redDark redDark,
} from '@radix-ui/colors' } from "@radix-ui/colors";
export const { styled, css, globalCss, keyframes, getCssText, theme, createTheme, config } = export const {
createStitches({ styled,
theme: { css,
colors: { globalCss,
...gray, keyframes,
...blue, getCssText,
...crimson, theme,
...grass, createTheme,
...slate, config,
...mauve, } = createStitches({
...mauveA, theme: {
...amber, colors: {
...purple, ...gray,
...green, ...blue,
...red, ...crimson,
accent: '#9D2DFF', ...grass,
background: '$gray1', ...slate,
backgroundAlt: '$gray4', ...mauve,
backgroundOverlay: '$mauve2', ...mauveA,
text: '$gray12', ...amber,
textMuted: '$gray10', ...purple,
primary: '$plum', ...green,
error: '$red9', ...red,
warning: '$amber11', accent: "#9D2DFF",
success: '$grass11', background: "$gray1",
white: 'white', backgroundAlt: "$gray4",
black: 'black', backgroundOverlay: "$mauve2",
deep: 'rgb(244, 244, 244)' text: "$gray12",
}, textMuted: "$gray10",
fonts: { primary: "$plum",
body: 'Work Sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif', error: '$red9',
heading: 'Work Sans, sans-serif', warning: '$amber11',
monospace: 'Roboto Mono, monospace' success: "$grass11",
}, white: "white",
fontSizes: { black: "black",
xs: '0.6875rem', deep: "rgb(244, 244, 244)",
sm: '0.875rem',
md: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
'5xl': '3rem',
'6xl': '3.75rem',
'7xl': '4.5rem',
'8xl': '6rem',
'9xl': '8rem',
default: '$md'
},
space: {
px: '1px',
0.5: '0.125rem',
1: '0.25rem',
1.5: '0.375rem',
2: '0.5rem',
2.5: '0.625rem',
3: '0.75rem',
3.5: '0.875rem',
4: '1rem',
5: '1.25rem',
6: '1.5rem',
7: '1.75rem',
8: '2rem',
9: '2.25rem',
10: '2.5rem',
12: '3rem',
14: '3.5rem',
16: '4rem',
20: '5rem',
24: '6rem',
28: '7rem',
32: '8rem',
36: '9rem',
40: '10rem',
44: '11rem',
48: '12rem',
52: '13rem',
56: '14rem',
60: '15rem',
64: '16rem',
72: '18rem',
80: '20rem',
96: '24rem',
widePlus: '2048px',
wide: '1536px',
layoutPlus: '1260px',
layout: '1024px',
copyUltra: '980px',
copyPlus: '768px',
copy: '680px',
narrowPlus: '600px',
narrow: '512px',
xs: '20rem',
sm: '24rem',
md: '28rem',
lg: '32rem',
xl: '36rem',
'2xl': '42rem',
'3xl': '48rem',
'4xl': '56rem',
'5xl': '64rem',
'6xl': '72rem',
'7xl': '80rem',
'8xl': '90rem'
},
sizes: {
px: '1px',
0.5: '0.125rem',
1: '0.25rem',
1.5: '0.375rem',
2: '0.5rem',
2.5: '0.625rem',
3: '0.75rem',
3.5: '0.875rem',
4: '1rem',
5: '1.25rem',
6: '1.5rem',
7: '1.75rem',
8: '2rem',
9: '2.25rem',
10: '2.5rem',
12: '3rem',
14: '3.5rem',
16: '4rem',
20: '5rem',
24: '6rem',
28: '7rem',
32: '8rem',
36: '9rem',
40: '10rem',
44: '11rem',
48: '12rem',
52: '13rem',
56: '14rem',
60: '15rem',
64: '16rem',
72: '18rem',
80: '20rem',
96: '24rem',
max: 'max-content',
min: 'min-content',
full: '100%',
'3xs': '14rem',
'2xs': '16rem',
xs: '20rem',
sm: '24rem',
md: '28rem',
lg: '32rem',
xl: '36rem',
'2xl': '42rem',
'3xl': '48rem',
'4xl': '56rem',
'5xl': '64rem',
'6xl': '72rem',
'7xl': '80rem',
'8xl': '90rem'
},
radii: {
none: '0',
sm: '0.2rem',
base: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
xl: '0.75rem',
'2xl': '1rem',
'3xl': '1.5rem',
full: '9999px'
},
fontWeights: {
body: 400,
heading: 700,
bold: 700
},
lineHeights: {
one: 1,
body: 1.5,
heading: 0.85
},
letterSpacings: {},
borderWidths: {},
borderStyles: {},
shadows: {},
zIndices: {},
transitions: {}
}, },
media: { fonts: {
sm: '(min-width: 30em)', body: 'Work Sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
md: '(min-width: 48em)', heading: "Work Sans, sans-serif",
lg: '(min-width: 62em)', monospace: "Roboto Mono, monospace",
xl: '(min-width: 80em)',
'2xl': '(min-width: 96em)',
hover: '(any-hover: hover)',
dark: '(prefers-color-scheme: dark)',
light: '(prefers-color-scheme: light)'
}, },
utils: { fontSizes: {
// Abbreviated margin properties xs: "0.6875rem",
m: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'margin'>) => ({ sm: "0.875rem",
margin: value md: "1rem",
}), lg: "1.125rem",
mt: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginTop'>) => ({ xl: "1.25rem",
marginTop: value "2xl": "1.5rem",
}), "3xl": "1.875rem",
mr: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginRight'>) => ({ "4xl": "2.25rem",
marginRight: value "5xl": "3rem",
}), "6xl": "3.75rem",
mb: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginBottom'>) => ({ "7xl": "4.5rem",
marginBottom: value "8xl": "6rem",
}), "9xl": "8rem",
ml: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginLeft'>) => ({ default: "$md",
marginLeft: value },
}), space: {
mx: ( px: "1px",
value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginLeft' | 'marginRight'> 0.5: "0.125rem",
) => ({ 1: "0.25rem",
marginLeft: value, 1.5: "0.375rem",
marginRight: value 2: "0.5rem",
}), 2.5: "0.625rem",
my: ( 3: "0.75rem",
value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginTop' | 'marginBottom'> 3.5: "0.875rem",
) => ({ 4: "1rem",
marginTop: value, 5: "1.25rem",
marginBottom: value 6: "1.5rem",
}), 7: "1.75rem",
// Abbreviated margin properties 8: "2rem",
p: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'padding'>) => ({ 9: "2.25rem",
padding: value 10: "2.5rem",
}), 12: "3rem",
pt: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingTop'>) => ({ 14: "3.5rem",
paddingTop: value 16: "4rem",
}), 20: "5rem",
pr: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingRight'>) => ({ 24: "6rem",
paddingRight: value 28: "7rem",
}), 32: "8rem",
pb: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingBottom'>) => ({ 36: "9rem",
paddingBottom: value 40: "10rem",
}), 44: "11rem",
pl: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingLeft'>) => ({ 48: "12rem",
paddingLeft: value 52: "13rem",
}), 56: "14rem",
px: ( 60: "15rem",
value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingLeft' | 'paddingRight'> 64: "16rem",
) => ({ 72: "18rem",
paddingLeft: value, 80: "20rem",
paddingRight: value 96: "24rem",
}), widePlus: "2048px",
py: ( wide: "1536px",
value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingTop' | 'paddingBottom'> layoutPlus: "1260px",
) => ({ layout: "1024px",
paddingTop: value, copyUltra: "980px",
paddingBottom: value copyPlus: "768px",
}), copy: "680px",
narrowPlus: "600px",
narrow: "512px",
xs: "20rem",
sm: "24rem",
md: "28rem",
lg: "32rem",
xl: "36rem",
"2xl": "42rem",
"3xl": "48rem",
"4xl": "56rem",
"5xl": "64rem",
"6xl": "72rem",
"7xl": "80rem",
"8xl": "90rem",
},
sizes: {
px: "1px",
0.5: "0.125rem",
1: "0.25rem",
1.5: "0.375rem",
2: "0.5rem",
2.5: "0.625rem",
3: "0.75rem",
3.5: "0.875rem",
4: "1rem",
5: "1.25rem",
6: "1.5rem",
7: "1.75rem",
8: "2rem",
9: "2.25rem",
10: "2.5rem",
12: "3rem",
14: "3.5rem",
16: "4rem",
20: "5rem",
24: "6rem",
28: "7rem",
32: "8rem",
36: "9rem",
40: "10rem",
44: "11rem",
48: "12rem",
52: "13rem",
56: "14rem",
60: "15rem",
64: "16rem",
72: "18rem",
80: "20rem",
96: "24rem",
max: "max-content",
min: "min-content",
full: "100%",
"3xs": "14rem",
"2xs": "16rem",
xs: "20rem",
sm: "24rem",
md: "28rem",
lg: "32rem",
xl: "36rem",
"2xl": "42rem",
"3xl": "48rem",
"4xl": "56rem",
"5xl": "64rem",
"6xl": "72rem",
"7xl": "80rem",
"8xl": "90rem",
},
radii: {
none: "0",
sm: "0.2rem",
base: "0.25rem",
md: "0.375rem",
lg: "0.5rem",
xl: "0.75rem",
"2xl": "1rem",
"3xl": "1.5rem",
full: "9999px",
},
fontWeights: {
body: 400,
heading: 700,
bold: 700,
},
lineHeights: {
one: 1,
body: 1.5,
heading: 0.85,
},
letterSpacings: {},
borderWidths: {},
borderStyles: {},
shadows: {},
zIndices: {},
transitions: {},
},
media: {
sm: "(min-width: 30em)",
md: "(min-width: 48em)",
lg: "(min-width: 62em)",
xl: "(min-width: 80em)",
"2xl": "(min-width: 96em)",
hover: "(any-hover: hover)",
dark: "(prefers-color-scheme: dark)",
light: "(prefers-color-scheme: light)",
},
utils: {
// Abbreviated margin properties
m: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"margin">
) => ({
margin: value,
}),
mt: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"marginTop">
) => ({
marginTop: value,
}),
mr: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"marginRight">
) => ({
marginRight: value,
}),
mb: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"marginBottom">
) => ({
marginBottom: value,
}),
ml: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"marginLeft">
) => ({
marginLeft: value,
}),
mx: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"marginLeft" | "marginRight">
) => ({
marginLeft: value,
marginRight: value,
}),
my: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"marginTop" | "marginBottom">
) => ({
marginTop: value,
marginBottom: value,
}),
// Abbreviated margin properties
p: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"padding">
) => ({
padding: value,
}),
pt: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"paddingTop">
) => ({
paddingTop: value,
}),
pr: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingRight">
) => ({
paddingRight: value,
}),
pb: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingBottom">
) => ({
paddingBottom: value,
}),
pl: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingLeft">
) => ({
paddingLeft: value,
}),
px: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingLeft" | "paddingRight">
) => ({
paddingLeft: value,
paddingRight: value,
}),
py: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingTop" | "paddingBottom">
) => ({
paddingTop: value,
paddingBottom: value,
}),
// A property for applying width/height together // A property for applying width/height together
size: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'width' | 'height'>) => ({ size: (
width: value, value:
height: value | Stitches.ScaleValue<"space">
}), | Stitches.PropertyValue<"width" | "height">
// color: (value: Stitches.PropertyValue<'color'> | Stitches.PropertyValue<'width' | 'height'> => ({ ) => ({
// color: value width: value,
// }), height: value,
}),
// color: (value: Stitches.PropertyValue<'color'> | Stitches.PropertyValue<'width' | 'height'> => ({
// color: value
// }),
// A property to apply linear gradient // A property to apply linear gradient
linearGradient: (value: Stitches.ScaleValue<'space'>) => ({ linearGradient: (value: Stitches.ScaleValue<"space">) => ({
backgroundImage: `linear-gradient(${value})` backgroundImage: `linear-gradient(${value})`,
}), }),
// An abbreviated property for border-radius // An abbreviated property for border-radius
br: (value: Stitches.ScaleValue<'space'>) => ({ br: (value: Stitches.ScaleValue<"space">) => ({
borderRadius: value borderRadius: value,
}) }),
} },
}) });
export const darkTheme = createTheme('dark', { export const darkTheme = createTheme("dark", {
colors: { colors: {
...grayDark, ...grayDark,
...blueDark, ...blueDark,
@@ -315,24 +365,24 @@ export const darkTheme = createTheme('dark', {
...purpleDark, ...purpleDark,
...greenDark, ...greenDark,
...redDark, ...redDark,
deep: 'rgb(10, 10, 10)', deep: "rgb(10, 10, 10)",
backgroundOverlay: '$mauve5' backgroundOverlay: "$mauve5"
// backgroundA: transparentize(0.1, grayDark.gray1), // backgroundA: transparentize(0.1, grayDark.gray1),
} },
}) });
export const globalStyles = globalCss({ export const globalStyles = globalCss({
// body: { backgroundColor: '$background', color: '$text', fontFamily: 'Helvetica' }, // body: { backgroundColor: '$background', color: '$text', fontFamily: 'Helvetica' },
'html, body': { "html, body": {
backgroundColor: '$mauve2', backgroundColor: "$mauve2",
color: '$mauve12', color: "$mauve12",
fontFamily: '$body', fontFamily: "$body",
fontSize: '$md', fontSize: "$md",
'-webkit-font-smoothing': 'antialiased', "-webkit-font-smoothing": "antialiased",
'-moz-osx-font-smoothing': 'grayscale' "-moz-osx-font-smoothing": "grayscale",
}, },
a: { a: {
color: 'inherit', color: "inherit",
textDecoration: 'none' textDecoration: "none",
} },
}) });

View File

@@ -1,21 +1,21 @@
import { keyframes } from '../stitches.config' import { keyframes } from '../stitches.config';
export const slideUpAndFade = keyframes({ export 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)" },
}) });
export const slideRightAndFade = keyframes({ export 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)" },
}) });
export const slideDownAndFade = keyframes({ export 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)" },
}) });
export const slideLeftAndFade = keyframes({ export 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)" },
}) });

View File

@@ -309,4 +309,4 @@
"editorCursor.foreground": "#A7A7A7", "editorCursor.foreground": "#A7A7A7",
"editorWhitespace.foreground": "#CAE2FB3D" "editorWhitespace.foreground": "#CAE2FB3D"
} }
} }

View File

@@ -193,4 +193,4 @@
"editorCursor.foreground": "#FFFFFF", "editorCursor.foreground": "#FFFFFF",
"editorWhitespace.foreground": "#404040" "editorWhitespace.foreground": "#404040"
} }
} }

View File

@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -16,6 +20,13 @@
"incremental": true, "incremental": true,
"noUnusedLocals": true "noUnusedLocals": true
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": [
"exclude": ["node_modules", "*.md"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules",
"*.md"
]
} }

10
types/next-auth.d.ts vendored
View File

@@ -1,13 +1,13 @@
import NextAuth, { User } from 'next-auth' import NextAuth, { User } from "next-auth"
declare module 'next-auth' { declare module "next-auth" {
/** /**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/ */
interface Session { interface Session {
user: User & { user: User & {
username: string username: string;
} }
accessToken?: string accessToken?: string;
} }
} }

View File

@@ -1,20 +1,24 @@
import { Spec, parse, Problem } from 'comment-parser' import { Spec, parse, Problem } from "comment-parser"
export const getTags = (source?: string): Spec[] => { export const getTags = (source?: string): Spec[] => {
if (!source) return [] if (!source) return []
const blocks = parse(source) const blocks = parse(source)
const tags = blocks.reduce((acc, block) => acc.concat(block.tags), [] as Spec[]) const tags = blocks.reduce(
return tags (acc, block) => acc.concat(block.tags),
[] as Spec[]
);
return tags
} }
export const getErrors = (source?: string): Error | undefined => { export const getErrors = (source?: string): Error | undefined => {
if (!source) return undefined if (!source) return undefined
const blocks = parse(source) const blocks = parse(source)
const probs = blocks.reduce((acc, block) => acc.concat(block.problems), [] as Problem[]) const probs = blocks.reduce(
if (!probs.length) return undefined (acc, block) => acc.concat(block.problems),
const errors = probs.map(prob => `[${prob.code}] on line ${prob.line}: ${prob.message}`) [] as Problem[]
const error = new Error( );
`The following error(s) occurred while parsing JSDOC: \n${errors.join('\n')}` if (!probs.length) return undefined
) const errors = probs.map(prob => `[${prob.code}] on line ${prob.line}: ${prob.message}`)
return error const error = new Error(`The following error(s) occurred while parsing JSDOC: \n${errors.join('\n')}`)
} return error
}

View File

@@ -1,15 +1,15 @@
import { decodeRestrictedBase64ToBytes } from './decodeRestrictedBase64ToBytes' import { decodeRestrictedBase64ToBytes } from "./decodeRestrictedBase64ToBytes";
import { isZlibData, decompressZlib } from './zlib' import { isZlibData, decompressZlib } from "./zlib";
import { fromByteArray } from 'base64-js' import { fromByteArray } from "base64-js";
export async function decodeBinary(input: string): Promise<ArrayBuffer> { export async function decodeBinary(input: string): Promise<ArrayBuffer> {
let data = decodeRestrictedBase64ToBytes(input) let data = decodeRestrictedBase64ToBytes(input);
if (isZlibData(data)) { if (isZlibData(data)) {
data = await decompressZlib(data) data = await decompressZlib(data);
} }
return data.buffer as ArrayBuffer return data.buffer as ArrayBuffer;
} }
export function encodeBinary(input: ArrayBuffer): string { export function encodeBinary(input: ArrayBuffer): string {
return fromByteArray(new Uint8Array(input)) return fromByteArray(new Uint8Array(input));
} }

View File

@@ -1,119 +1,44 @@
const base64DecodeMap = [ const base64DecodeMap = [ // starts at 0x2B
// starts at 0x2B 62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
62, 0, 0, 0, 0, 0, 0, 0, // 0x3A-0x40
0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
0, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, // 0x5B-0x0x60
0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
63, 44, 45, 46, 47, 48, 49, 50, 51
52, ];
53,
54,
55,
56,
57,
58,
59,
60,
61,
0,
0,
0,
0,
0,
0,
0, // 0x3A-0x40
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
0,
0,
0,
0,
0,
0, // 0x5B-0x0x60
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51
]
const base64DecodeMapOffset = 0x2b const base64DecodeMapOffset = 0x2B;
const base64EOF = 0x3d const base64EOF = 0x3D;
export function decodeRestrictedBase64ToBytes(encoded: string) { export function decodeRestrictedBase64ToBytes(encoded: string) {
let ch: any let ch: any;
let code: any let code: any;
let code2: any let code2: any;
const len = encoded.length const len = encoded.length;
const padding = encoded.charAt(len - 2) === '=' ? 2 : encoded.charAt(len - 1) === '=' ? 1 : 0 const padding = encoded.charAt(len - 2) === "=" ? 2 : encoded.charAt(len - 1) === "=" ? 1 : 0;
const decoded = new Uint8Array((encoded.length >> 2) * 3 - padding) const decoded = new Uint8Array((encoded.length >> 2) * 3 - padding);
for (let i = 0, j = 0; i < encoded.length; ) { for (let i = 0, j = 0; i < encoded.length;) {
ch = encoded.charCodeAt(i++) ch = encoded.charCodeAt(i++);
code = base64DecodeMap[ch - base64DecodeMapOffset] code = base64DecodeMap[ch - base64DecodeMapOffset];
ch = encoded.charCodeAt(i++) ch = encoded.charCodeAt(i++);
code2 = base64DecodeMap[ch - base64DecodeMapOffset] code2 = base64DecodeMap[ch - base64DecodeMapOffset];
decoded[j++] = (code << 2) | ((code2 & 0x30) >> 4) decoded[j++] = (code << 2) | ((code2 & 0x30) >> 4);
ch = encoded.charCodeAt(i++) ch = encoded.charCodeAt(i++);
if (ch === base64EOF) { if (ch === base64EOF) {
return decoded return decoded;
} }
code = base64DecodeMap[ch - base64DecodeMapOffset] code = base64DecodeMap[ch - base64DecodeMapOffset];
decoded[j++] = ((code2 & 0x0f) << 4) | ((code & 0x3c) >> 2) decoded[j++] = ((code2 & 0x0f) << 4) | ((code & 0x3c) >> 2);
ch = encoded.charCodeAt(i++) ch = encoded.charCodeAt(i++);
if (ch === base64EOF) { if (ch === base64EOF) {
return decoded return decoded;
} }
code2 = base64DecodeMap[ch - base64DecodeMapOffset] code2 = base64DecodeMap[ch - base64DecodeMapOffset];
decoded[j++] = ((code & 0x03) << 6) | code2 decoded[j++] = ((code & 0x03) << 6) | code2;
} }
return decoded return decoded;
} }

View File

@@ -1,39 +1,30 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast';
import { derive, sign } from 'xrpl-accountlib' import { derive, sign } from "xrpl-accountlib"
import state, { IAccount } from '../state' import state, { IAccount } from "../state"
const estimateFee = async ( const estimateFee = async (tx: Record<string, unknown>, account: IAccount, opts: { silent?: boolean } = {}): Promise<null | { base_fee: string, median_fee: string; minimum_fee: string; open_ledger_fee: string; }> => {
tx: Record<string, unknown>,
account: IAccount,
opts: { silent?: boolean } = {}
): Promise<null | {
base_fee: string
median_fee: string
minimum_fee: string
open_ledger_fee: string
}> => {
try { try {
const copyTx = JSON.parse(JSON.stringify(tx)) const copyTx = JSON.parse(JSON.stringify(tx))
delete copyTx['SigningPubKey'] delete copyTx['SigningPubKey']
if (!copyTx.Fee) { if (!copyTx.Fee) {
copyTx.Fee = '1000' copyTx.Fee = '1000'
} }
const keypair = derive.familySeed(account.secret) const keypair = derive.familySeed(account.secret)
const { signedTransaction } = sign(copyTx, keypair) const { signedTransaction } = sign(copyTx, keypair);
const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction }) const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction })
if (res && res.drops) { if (res && res.drops) {
return res.drops return res.drops;
} }
return null return null
} catch (err) { } catch (err) {
if (!opts.silent) { if (!opts.silent) {
console.error(err) console.error(err)
toast.error('Cannot estimate fee.') // ? Some better msg toast.error("Cannot estimate fee.") // ? Some better msg
} }
return null return null
} }
} }
export default estimateFee export default estimateFee

View File

@@ -1,15 +1,15 @@
interface File { interface File {
name: string name: string
} }
export const guessZipFileName = (files: File[]) => { export const guessZipFileName = (files: File[]) => {
let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.') let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
parts = parts.length > 1 ? parts.slice(0, -1) : parts parts = parts.length > 1 ? parts.slice(0, -1) : parts
return parts.join('') return parts.join('')
} }
export const capitalize = (value?: string) => { export const capitalize = (value?: string) => {
if (!value) return '' if (!value) return '';
return value[0].toLocaleUpperCase() + value.slice(1) return value[0].toLocaleUpperCase() + value.slice(1);
} }

View File

@@ -24,22 +24,23 @@ export const tts = {
ttNFTOKEN_CREATE_OFFER: 27, ttNFTOKEN_CREATE_OFFER: 27,
ttNFTOKEN_CANCEL_OFFER: 28, ttNFTOKEN_CANCEL_OFFER: 28,
ttNFTOKEN_ACCEPT_OFFER: 29 ttNFTOKEN_ACCEPT_OFFER: 29
} };
export type TTS = typeof tts export type TTS = typeof tts;
const calculateHookOn = (arr: (keyof TTS)[]) => { const calculateHookOn = (arr: (keyof TTS)[]) => {
let start = '0x000000003e3ff5bf' let start = '0x000000003e3ff5bf';
arr.forEach(n => { arr.forEach(n => {
let v = BigInt(start) let v = BigInt(start);
v ^= BigInt(1) << BigInt(tts[n as keyof TTS]) v ^= (BigInt(1) << BigInt(tts[n as keyof TTS]));
let s = v.toString(16) let s = v.toString(16);
let l = s.length let l = s.length;
if (l < 16) s = '0'.repeat(16 - l) + s if (l < 16)
s = '0x' + s s = '0'.repeat(16 - l) + s;
start = s s = '0x' + s;
start = s;
}) })
return start.substring(2) return start.substring(2);
} }
export default calculateHookOn export default calculateHookOn

Some files were not shown because too many files have changed in this diff Show More