mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-08 13:10:44 +08:00
Refactor forms
- add a link to the faucet page from `Rinkeby` badge in header
- refactor the `<TextInput>` component to include
- a loading indicator
- a keyboard shortcut to submit the input by pressing the `enter` key
- add a keyboard shortcut hint <kbd>⏎</kbd> that shows up whenever the
input is ready to be submitted
- a validation message
- align all pages with a single input form to have a similar look and feel
- improve validation messages and error handling in all forms
Signed-off-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
+3
-1
@@ -190,7 +190,9 @@
|
||||
|
||||
<div class="right">
|
||||
{#if config && config.network.name === "rinkeby"}
|
||||
<span class="network">Rinkeby</span>
|
||||
<a use:link href="/faucet">
|
||||
<span class="network">Rinkeby</span>
|
||||
</a>
|
||||
{:else if config && config.network.name === "homestead"}
|
||||
<!-- Don't show anything -->
|
||||
{:else}
|
||||
|
||||
+6
-7
@@ -1,21 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from "svelte";
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
export let small = false;
|
||||
export let center = false;
|
||||
export let fadeIn = false;
|
||||
export let margins = false;
|
||||
export let condensed = false;
|
||||
export let noDelay = false;
|
||||
|
||||
let show: boolean = false;
|
||||
|
||||
const timeout = window.setTimeout(() => {
|
||||
if (noDelay) {
|
||||
show = true;
|
||||
}, 200);
|
||||
|
||||
onDestroy(() => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
} else {
|
||||
debounce(() => (show = true), 200)();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
+46
-48
@@ -128,7 +128,6 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { navigate } from "svelte-routing";
|
||||
|
||||
import Loading from "@app/Loading.svelte";
|
||||
import TextInput from "@app/TextInput.svelte";
|
||||
import { unreachable } from "@app/utils";
|
||||
|
||||
@@ -148,49 +147,52 @@
|
||||
debounce(() => (shaking = false), 500)();
|
||||
}
|
||||
|
||||
async function search(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
if (input === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
const query = input;
|
||||
const searchResult = await searchProjectsAndProfiles(input, config);
|
||||
|
||||
if (searchResult.type === "nothing") {
|
||||
shake();
|
||||
} else if (searchResult.type === "error") {
|
||||
// TODO: show some kind of notification to the user.
|
||||
shake();
|
||||
} else if (searchResult.type === "singleProfile") {
|
||||
input = "";
|
||||
navigate(`/${searchResult.id}`, { replace: true });
|
||||
dispatch("finished");
|
||||
} else if (searchResult.type === "singleProject") {
|
||||
input = "";
|
||||
navigate(`/seeds/${searchResult.seedHost}/${searchResult.id}`, {
|
||||
replace: true,
|
||||
});
|
||||
dispatch("finished");
|
||||
} else if (searchResult.type === "projectsAndProfiles") {
|
||||
// TODO: show some kind of notification about any errors to the user.
|
||||
input = "";
|
||||
dispatch("search", {
|
||||
query,
|
||||
results: searchResult.projectsAndProfiles,
|
||||
});
|
||||
dispatch("finished");
|
||||
} else {
|
||||
unreachable(searchResult);
|
||||
}
|
||||
loading = false;
|
||||
async function search() {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
const query = input;
|
||||
const searchResult = await searchProjectsAndProfiles(input, config);
|
||||
|
||||
if (searchResult.type === "nothing") {
|
||||
shake();
|
||||
} else if (searchResult.type === "error") {
|
||||
// TODO: show some kind of notification to the user.
|
||||
shake();
|
||||
} else if (searchResult.type === "singleProfile") {
|
||||
input = "";
|
||||
navigate(`/${searchResult.id}`, { replace: true });
|
||||
dispatch("finished");
|
||||
} else if (searchResult.type === "singleProject") {
|
||||
input = "";
|
||||
navigate(`/seeds/${searchResult.seedHost}/${searchResult.id}`, {
|
||||
replace: true,
|
||||
});
|
||||
dispatch("finished");
|
||||
} else if (searchResult.type === "projectsAndProfiles") {
|
||||
// TODO: show some kind of notification about any errors to the user.
|
||||
input = "";
|
||||
dispatch("search", {
|
||||
query,
|
||||
results: searchResult.projectsAndProfiles,
|
||||
});
|
||||
dispatch("finished");
|
||||
} else {
|
||||
unreachable(searchResult);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
$: valid = input !== "";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.search {
|
||||
display: flex;
|
||||
}
|
||||
.shaking {
|
||||
animation: horizontal-shaking 0.35s;
|
||||
}
|
||||
@@ -213,17 +215,13 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class:shaking>
|
||||
<div class="search" class:shaking>
|
||||
<TextInput
|
||||
variant="dashed"
|
||||
valid={input !== ""}
|
||||
{loading}
|
||||
disabled={loading}
|
||||
bind:value={input}
|
||||
on:keydown={search}
|
||||
placeholder="Search a name or address…">
|
||||
<svelte:fragment slot="right">
|
||||
{#if loading}
|
||||
<Loading small />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TextInput>
|
||||
on:submit={search}
|
||||
placeholder="Search a name or address…" />
|
||||
</div>
|
||||
|
||||
+75
-20
@@ -1,6 +1,9 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts" strictEvents>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import Loading from "@app/Loading.svelte";
|
||||
|
||||
export let name: string | undefined = undefined;
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let value: string | undefined = undefined;
|
||||
@@ -9,15 +12,29 @@
|
||||
|
||||
export let autofocus: boolean = false;
|
||||
export let disabled: boolean = false;
|
||||
export let loading: boolean = false;
|
||||
export let valid: boolean = false;
|
||||
export let validationMessage: string | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
submit: boolean;
|
||||
}>();
|
||||
|
||||
let rightContainerWidth: number;
|
||||
let inputElement: HTMLInputElement | undefined = undefined;
|
||||
|
||||
onMount(() => {
|
||||
if (autofocus && inputElement) {
|
||||
// We set preventScroll to true for Svelte animations to work.
|
||||
inputElement.focus({ preventScroll: true });
|
||||
}
|
||||
});
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
dispatch("submit");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -26,6 +43,8 @@
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 2.5rem;
|
||||
}
|
||||
input {
|
||||
background: transparent;
|
||||
@@ -37,6 +56,7 @@
|
||||
margin: 0;
|
||||
outline: none;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
input::placeholder {
|
||||
color: var(--color-secondary);
|
||||
@@ -66,30 +86,65 @@
|
||||
height: var(--button-regular-height);
|
||||
padding-right: 1rem;
|
||||
padding-left: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.validation-message {
|
||||
color: var(--color-negative);
|
||||
font-size: var(--font-size-small);
|
||||
margin-left: 1rem;
|
||||
position: relative;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.validation-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.key-hint {
|
||||
border-radius: 0.25rem;
|
||||
color: var(--color-secondary);
|
||||
background-color: var(--color-secondary-2);
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrapper">
|
||||
<input
|
||||
class:regular={variant === "regular"}
|
||||
class:dashed={variant === "dashed"}
|
||||
style:padding-right={rightContainerWidth
|
||||
? `${rightContainerWidth}px`
|
||||
: "auto"}
|
||||
bind:this={inputElement}
|
||||
type="text"
|
||||
{name}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
bind:value
|
||||
on:input
|
||||
on:keydown|stopPropagation
|
||||
on:click
|
||||
on:change />
|
||||
<div class="validation-wrapper">
|
||||
<input
|
||||
class:regular={variant === "regular"}
|
||||
class:dashed={variant === "dashed"}
|
||||
style:padding-right={rightContainerWidth
|
||||
? `${rightContainerWidth}px`
|
||||
: "auto"}
|
||||
bind:this={inputElement}
|
||||
type="text"
|
||||
{name}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
bind:value
|
||||
on:input
|
||||
on:keydown|stopPropagation={handleKeydown}
|
||||
on:click
|
||||
on:change />
|
||||
|
||||
{#if $$slots.right}
|
||||
<div class="right-container" bind:clientWidth={rightContainerWidth}>
|
||||
<slot name="right" />
|
||||
{#if $$slots.right}
|
||||
<slot name="right" />
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<Loading small noDelay />
|
||||
{/if}
|
||||
|
||||
{#if valid && !loading}
|
||||
<div class="key-hint">⏎</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if validationMessage}
|
||||
<div class="validation-message">
|
||||
{validationMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+123
-110
@@ -1,24 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { Config } from "@app/config";
|
||||
import type { BigNumber } from "@ethersproject/bignumber";
|
||||
|
||||
import { session } from "@app/session";
|
||||
import { setOpenGraphMetaTag, toWei } from "@app/utils";
|
||||
import { formatEther } from "@ethersproject/units";
|
||||
import { navigate } from "svelte-routing";
|
||||
import {
|
||||
calculateTimeLock,
|
||||
getMaxWithdrawAmount,
|
||||
lastWithdrawalByUser,
|
||||
calculateTimeLock,
|
||||
} from "./lib";
|
||||
import Button from "@app/Button.svelte";
|
||||
import TextInput from "@app/TextInput.svelte";
|
||||
|
||||
export let config: Config;
|
||||
|
||||
let amount: string;
|
||||
let maxWithdrawAmount: BigNumber;
|
||||
let lastWithdrawal: BigNumber;
|
||||
let error: string | undefined;
|
||||
let amount: string = "";
|
||||
let loading: boolean = false;
|
||||
let validationMessage: string | undefined = undefined;
|
||||
let valid: boolean = false;
|
||||
|
||||
setOpenGraphMetaTag([
|
||||
{ prop: "og:title", content: "Radicle Faucet" },
|
||||
@@ -26,132 +26,145 @@
|
||||
{ prop: "og:url", content: window.location.href },
|
||||
]);
|
||||
|
||||
async function withdraw() {
|
||||
const [state, message] = await isAbleToWithdraw(amount);
|
||||
if (state === true) navigate("/faucet/withdraw", { state: { amount } });
|
||||
else error = message;
|
||||
}
|
||||
async function withdraw(amount: string) {
|
||||
if (!valid || !$session) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function isAbleToWithdraw(amount: string): Promise<[boolean, string?]> {
|
||||
loading = true;
|
||||
try {
|
||||
if (!$session) {
|
||||
return [false];
|
||||
}
|
||||
if (!amount || amount === "0") {
|
||||
return [false, "Not able to withdraw zero tokens"];
|
||||
}
|
||||
if (toWei(amount).gt(maxWithdrawAmount)) {
|
||||
return [
|
||||
false,
|
||||
`Reduce amount, max withdrawal is ${formatEther(maxWithdrawAmount)}`,
|
||||
];
|
||||
}
|
||||
const currentTime = new Date().getTime();
|
||||
const timelock = await calculateTimeLock(amount, $session.signer, config);
|
||||
// Converting a 10 digit to 13 digit timestamp by multiplying by 1000
|
||||
// since JS doesn't display a correct Date string when passing a 10 digit timestamp.
|
||||
const nextAvailableWithdraw = lastWithdrawal.add(timelock).mul(1000);
|
||||
if (nextAvailableWithdraw.gt(currentTime)) {
|
||||
return [
|
||||
false,
|
||||
`Not ready to withdraw, return after ${new Date(
|
||||
nextAvailableWithdraw.toNumber(),
|
||||
).toLocaleString("en-GB")}`,
|
||||
];
|
||||
const lastWithdrawal = await lastWithdrawalByUser(
|
||||
$session.signer,
|
||||
config,
|
||||
);
|
||||
const maxWithdrawAmount = await getMaxWithdrawAmount(
|
||||
$session.signer,
|
||||
config,
|
||||
);
|
||||
|
||||
if (toWei(amount).gt(maxWithdrawAmount)) {
|
||||
validationMessage = `Reduce amount, max withdrawal is ${formatEther(
|
||||
maxWithdrawAmount,
|
||||
)}.`;
|
||||
return;
|
||||
}
|
||||
|
||||
return [true];
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
error = e.message;
|
||||
// Converting a 10 digit to 13 digit timestamp by multiplying by 1000
|
||||
// since JS doesn't display a correct Date string when passing a 10 digit
|
||||
// timestamp.
|
||||
const nextAvailableWithdraw = lastWithdrawal.add(timelock).mul(1000);
|
||||
if (nextAvailableWithdraw.gt(currentTime)) {
|
||||
validationMessage = `Not ready to withdraw, return after ${new Date(
|
||||
nextAvailableWithdraw.toNumber(),
|
||||
).toLocaleString("en-GB")}`;
|
||||
return;
|
||||
}
|
||||
|
||||
return [false];
|
||||
navigate("/faucet/withdraw", { state: { amount } });
|
||||
} catch (error) {
|
||||
validationMessage = "There was an error, check the dev console.";
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($session) {
|
||||
getMaxWithdrawAmount($session.signer, config).then(
|
||||
x => (maxWithdrawAmount = x),
|
||||
);
|
||||
lastWithdrawalByUser($session.signer, config).then(
|
||||
x => (lastWithdrawal = x),
|
||||
);
|
||||
function validate(amount: string) {
|
||||
if (amount === "") {
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
if (isNaN(Number(amount)) || Number(amount) <= 0) {
|
||||
return {
|
||||
valid: false,
|
||||
validationMessage: "Please enter a positive number.",
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
$: ({ valid, validationMessage } = validate(amount));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.input-caption {
|
||||
font-size: var(--font-size-medium);
|
||||
text-align: left;
|
||||
color: var(--color-secondary);
|
||||
margin-bottom: 2rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.input-main {
|
||||
main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: var(--color-secondary);
|
||||
flex-direction: column;
|
||||
}
|
||||
.error {
|
||||
padding-left: 1rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
.description :global(p) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.description.invalid {
|
||||
color: var(--color-negative) !important;
|
||||
}
|
||||
.name {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
padding-bottom: 24vh;
|
||||
padding-top: 5rem;
|
||||
width: 28rem;
|
||||
}
|
||||
.title {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
.form {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>Radicle: Faucet</title>
|
||||
<title>Radicle – Faucet</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="off-centered">
|
||||
<div>
|
||||
{#if config.network.name === "homestead"}
|
||||
<div class="input-caption">
|
||||
To get RAD tokens on <span class="txt-bold">
|
||||
{config.network.name}
|
||||
</span>
|
||||
, please check the known exchanges.
|
||||
</div>
|
||||
{:else if !$session}
|
||||
<div class="input-caption">
|
||||
To get RAD tokens on <span class="txt-bold">
|
||||
{config.network.name}
|
||||
</span>
|
||||
, please connect your wallet.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="input-caption">
|
||||
Obtain RAD tokens on <span class="txt-bold">
|
||||
{config.network.name}
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-main">
|
||||
<div class="name">
|
||||
<div style="width: 14.5rem;">
|
||||
<TextInput
|
||||
placeholder="Set amount to withdraw"
|
||||
bind:value={amount}
|
||||
on:input={() => (error = "")} />
|
||||
</div>
|
||||
<Button variant="primary" on:click={withdraw}>Withdraw</Button>
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="error description invalid txt-small faded">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<main>
|
||||
<div class="title">
|
||||
Obtain RAD tokens on <span class="txt-bold">
|
||||
{config.network.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if config.network.name === "homestead"}
|
||||
<div class="subtitle">
|
||||
To get RAD tokens on <span class="txt-bold">
|
||||
{config.network.name},
|
||||
</span>
|
||||
please
|
||||
<br />
|
||||
check
|
||||
<a class="link" href="https://docs.radicle.xyz/get-involved/obtain-rad">
|
||||
popular exchanges
|
||||
</a>
|
||||
​.
|
||||
</div>
|
||||
{:else if !$session}
|
||||
<div class="subtitle">
|
||||
To get RAD tokens on <span class="txt-bold">
|
||||
{config.network.name}
|
||||
</span>
|
||||
​,
|
||||
<br />
|
||||
please connect your wallet.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="form">
|
||||
<TextInput
|
||||
autofocus
|
||||
placeholder="Enter amount to withdraw"
|
||||
{validationMessage}
|
||||
on:submit={() => {
|
||||
withdraw(amount);
|
||||
}}
|
||||
bind:value={amount}
|
||||
{valid}
|
||||
{loading} />
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
on:click={() => withdraw(amount)}
|
||||
disabled={!valid || loading}>
|
||||
Withdraw
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
@@ -8,102 +8,108 @@
|
||||
export let config: Config;
|
||||
|
||||
let input = "";
|
||||
let valid: boolean = false;
|
||||
let validationMessage: string | undefined = undefined;
|
||||
|
||||
function register() {
|
||||
navigate(`/registrations/${label}/form`);
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
navigate(`/registrations/${ensName}/form`);
|
||||
}
|
||||
|
||||
function validate(input: string): string[] {
|
||||
const errors: string[] = [];
|
||||
function validate(input: string) {
|
||||
if (input === "") {
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
if (input && input.includes(".")) {
|
||||
errors.push("Please do not use dots as separators.");
|
||||
return {
|
||||
valid: false,
|
||||
validationMessage: "Please do not use dots as separators.",
|
||||
};
|
||||
}
|
||||
if (input && input.length < 2) {
|
||||
errors.push("Please enter a minimum of 2 characters.");
|
||||
return {
|
||||
valid: false,
|
||||
validationMessage: "Please enter a minimum of 2 characters.",
|
||||
};
|
||||
}
|
||||
if (input && input.length > 128) {
|
||||
errors.push("Please enter a maximum of 128 characters.");
|
||||
return {
|
||||
valid: false,
|
||||
validationMessage: "Please enter a maximum of 128 characters.",
|
||||
};
|
||||
}
|
||||
|
||||
return errors;
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
$: label = input.trim();
|
||||
$: errors = validate(label);
|
||||
$: ensName = input.trim();
|
||||
$: ({ valid, validationMessage } = validate(ensName));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div.input-caption {
|
||||
font-size: var(--font-size-medium);
|
||||
text-align: left;
|
||||
margin-left: 1.5rem;
|
||||
padding-left: 1.5rem;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
div.input-main {
|
||||
main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
margin-left: 1.5rem;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
padding-bottom: 24vh;
|
||||
padding-top: 5rem;
|
||||
width: 32rem;
|
||||
}
|
||||
.title {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
.name {
|
||||
margin: 1rem;
|
||||
width: 22rem;
|
||||
}
|
||||
.input-info {
|
||||
position: absolute;
|
||||
font-style: italic;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
.explainer {
|
||||
width: 28rem;
|
||||
margin: 1rem 0;
|
||||
.form {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>Radicle: Register</title>
|
||||
<title>Radicle – Register</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="off-centered">
|
||||
<div>
|
||||
<div class="input-caption">
|
||||
Register a <span class="txt-bold">{config.registrar.domain}</span>
|
||||
name
|
||||
<div class="txt-small explainer">
|
||||
Register a unique name with our ENS registrar, under the <span
|
||||
class="txt-bold">
|
||||
radicle.eth
|
||||
</span>
|
||||
domain (e.g. cloudhead.radicle.eth). Radicle names never expire and free
|
||||
to register.
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-main">
|
||||
<span class="name">
|
||||
<TextInput bind:value={input} autofocus>
|
||||
<svelte:fragment slot="right">
|
||||
.{config.registrar.domain}
|
||||
</svelte:fragment>
|
||||
</TextInput>
|
||||
{#if errors}
|
||||
<div class="input-info">
|
||||
{#each errors as error}
|
||||
<div>{error}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
<main>
|
||||
<div class="title">
|
||||
Register a <span class="txt-bold">{config.registrar.domain}</span>
|
||||
name
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled={!label || errors.length !== 0}
|
||||
variant="primary"
|
||||
size="regular"
|
||||
on:click={register}>
|
||||
Check
|
||||
</Button>
|
||||
</div>
|
||||
<div class="subtitle">
|
||||
Register a unique name with our ENS registrar, under <br />
|
||||
the
|
||||
<span class="txt-bold">radicle.eth</span>
|
||||
domain (e.g. cloudhead.radicle.eth).
|
||||
<br />
|
||||
Radicle names never expire and free to register.
|
||||
</div>
|
||||
|
||||
<div class="form">
|
||||
<TextInput
|
||||
bind:value={input}
|
||||
autofocus
|
||||
on:submit={register}
|
||||
{valid}
|
||||
{validationMessage}>
|
||||
<svelte:fragment slot="right">
|
||||
.{config.registrar.domain}
|
||||
</svelte:fragment>
|
||||
</TextInput>
|
||||
|
||||
<Button
|
||||
disabled={!valid}
|
||||
variant="primary"
|
||||
size="regular"
|
||||
on:click={register}>
|
||||
Check
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+153
-118
@@ -1,52 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { State, state } from "./state";
|
||||
import { getInfo, withdrawVested } from "./vesting";
|
||||
import type { VestingInfo } from "./vesting";
|
||||
import type { Session } from "@app/session";
|
||||
import type { Config } from "@app/config";
|
||||
import Modal from "@app/Modal.svelte";
|
||||
import type { VestingInfo } from "./vesting";
|
||||
|
||||
import * as utils from "@app/utils";
|
||||
import Address from "@app/Address.svelte";
|
||||
import { formatAddress, isAddressEqual } from "@app/utils";
|
||||
import Button from "@app/Button.svelte";
|
||||
import Modal from "@app/Modal.svelte";
|
||||
import TextInput from "@app/TextInput.svelte";
|
||||
import { state, getInfo, withdrawVested } from "./vesting";
|
||||
|
||||
export let config: Config;
|
||||
export let session: Session | null;
|
||||
|
||||
let contractAddress = "";
|
||||
let info: VestingInfo | null = null;
|
||||
let validationMessage: string | undefined = undefined;
|
||||
let valid: boolean = false;
|
||||
|
||||
async function loadContract(config: Config) {
|
||||
state.set(State.Loading);
|
||||
info = await getInfo(contractAddress, config);
|
||||
state.set(State.Idle);
|
||||
}
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
info = null;
|
||||
state.set(State.Idle);
|
||||
state.set("loading");
|
||||
try {
|
||||
info = await getInfo(contractAddress, config);
|
||||
} catch (error) {
|
||||
validationMessage =
|
||||
"Couldn't load contract, check dev console for details.";
|
||||
console.error(error);
|
||||
}
|
||||
state.set("idle");
|
||||
}
|
||||
|
||||
$: isBeneficiary =
|
||||
info && session && isAddressEqual(info.beneficiary, session.address);
|
||||
info && session && utils.isAddressEqual(info.beneficiary, session.address);
|
||||
|
||||
function validate(address: string) {
|
||||
if (address === "") {
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
if (!utils.isAddress(address)) {
|
||||
return {
|
||||
valid: false,
|
||||
validationMessage: "Please enter a valid Ethereum address.",
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
$: ({ valid, validationMessage } = validate(contractAddress));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.input-caption {
|
||||
font-size: var(--font-size-medium);
|
||||
text-align: left;
|
||||
margin-left: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
.input-main {
|
||||
main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
margin-left: 1.5rem;
|
||||
color: var(--color-secondary);
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
padding-bottom: 24vh;
|
||||
padding-top: 5rem;
|
||||
width: 38rem;
|
||||
}
|
||||
.title {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
.form {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
table {
|
||||
table-layout: fixed;
|
||||
@@ -63,95 +89,104 @@
|
||||
<title>Radicle – Vesting</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="off-centered">
|
||||
<div>
|
||||
{#if info}
|
||||
<Modal>
|
||||
<span slot="title">
|
||||
{contractAddress}
|
||||
</span>
|
||||
<span slot="body">
|
||||
{#if $state === State.Withdrawn}
|
||||
Tokens successfully withdrawn to {formatAddress(info.beneficiary)}.
|
||||
{:else}
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label">Beneficiary</td>
|
||||
<td>
|
||||
<Address
|
||||
{config}
|
||||
address={info.beneficiary}
|
||||
compact
|
||||
resolve />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Allocation</td>
|
||||
<td>
|
||||
{info.totalVesting}
|
||||
<span class="txt-bold">{info.symbol}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Withdrawn</td>
|
||||
<td>
|
||||
{info.withdrawn}
|
||||
<span class="txt-bold">{info.symbol}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Withdrawable</td>
|
||||
<td>
|
||||
{info.withdrawableBalance}
|
||||
<span class="txt-bold">{info.symbol}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{/if}
|
||||
</span>
|
||||
<span slot="actions">
|
||||
{#if isBeneficiary}
|
||||
{#if $state === State.WithdrawingSign}
|
||||
<Button disabled waiting={true} variant="primary">
|
||||
Waiting for signature…
|
||||
</Button>
|
||||
{:else if $state === State.Withdrawing}
|
||||
<Button disabled waiting={true} variant="primary">
|
||||
Withdrawing…
|
||||
</Button>
|
||||
{:else if $state === State.Idle}
|
||||
<Button
|
||||
on:click={() => withdrawVested(contractAddress, config)}
|
||||
variant="primary">
|
||||
Withdraw
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button on:click={reset} variant="primary">Back</Button>
|
||||
</span>
|
||||
</Modal>
|
||||
{:else}
|
||||
<div class="input-caption">
|
||||
Enter your Radicle <span class="txt-bold">vesting contract</span>
|
||||
address
|
||||
</div>
|
||||
<div class="input-main">
|
||||
<span class="name">
|
||||
<div style="width: 25rem;">
|
||||
<TextInput
|
||||
autofocus
|
||||
disabled={$state === State.Loading}
|
||||
bind:value={contractAddress} />
|
||||
</div>
|
||||
</span>
|
||||
<Button
|
||||
on:click={() => loadContract(config)}
|
||||
variant="primary"
|
||||
waiting={$state === State.Loading}
|
||||
disabled={$state === State.Loading}>
|
||||
Load
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
{#if info}
|
||||
<Modal>
|
||||
<span slot="title">
|
||||
{contractAddress}
|
||||
</span>
|
||||
|
||||
<span slot="body">
|
||||
{#if $state === "withdrawn"}
|
||||
Tokens successfully withdrawn to {utils.formatAddress(
|
||||
info.beneficiary,
|
||||
)}.
|
||||
{:else}
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label">Beneficiary</td>
|
||||
<td>
|
||||
<Address {config} address={info.beneficiary} compact resolve />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Allocation</td>
|
||||
<td>
|
||||
{info.totalVesting}
|
||||
<span class="txt-bold">{info.symbol}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Withdrawn</td>
|
||||
<td>
|
||||
{info.withdrawn}
|
||||
<span class="txt-bold">{info.symbol}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Withdrawable</td>
|
||||
<td>
|
||||
{info.withdrawableBalance}
|
||||
<span class="txt-bold">{info.symbol}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span slot="actions">
|
||||
{#if isBeneficiary}
|
||||
{#if $state === "withdrawingSign"}
|
||||
<Button disabled waiting={true} variant="primary">
|
||||
Waiting for signature…
|
||||
</Button>
|
||||
{:else if $state === "withdrawing"}
|
||||
<Button disabled waiting={true} variant="primary">
|
||||
Withdrawing…
|
||||
</Button>
|
||||
{:else if $state === "idle"}
|
||||
<Button
|
||||
on:click={() => withdrawVested(contractAddress, config)}
|
||||
variant="primary">
|
||||
Withdraw
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => {
|
||||
info = null;
|
||||
state.set("idle");
|
||||
}}
|
||||
variant="primary">
|
||||
Back
|
||||
</Button>
|
||||
</span>
|
||||
</Modal>
|
||||
{:else}
|
||||
<main>
|
||||
<div class="title">
|
||||
Your Radicle <span class="txt-bold">vesting contract</span>
|
||||
</div>
|
||||
|
||||
<div class="form">
|
||||
<TextInput
|
||||
autofocus
|
||||
placeholder="Enter vesting contract address"
|
||||
{valid}
|
||||
{validationMessage}
|
||||
loading={$state === "loading"}
|
||||
disabled={$state === "loading"}
|
||||
on:submit={() => {
|
||||
loadContract(config);
|
||||
}}
|
||||
bind:value={contractAddress} />
|
||||
|
||||
<Button
|
||||
on:click={() => loadContract(config)}
|
||||
variant="primary"
|
||||
waiting={$state === "loading"}
|
||||
disabled={!valid || $state === "loading"}>
|
||||
Load
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export enum State {
|
||||
Error = -1,
|
||||
Idle = 0,
|
||||
Loading = 1,
|
||||
Found = 2,
|
||||
NotFound = 3,
|
||||
WithdrawingSign = 4,
|
||||
Withdrawing = 5,
|
||||
Withdrawn = 6,
|
||||
}
|
||||
|
||||
export const state = writable(State.Idle);
|
||||
|
||||
state.subscribe(s => {
|
||||
console.debug("vesting.state", s);
|
||||
});
|
||||
+16
-10
@@ -1,9 +1,11 @@
|
||||
import { ethers } from "ethers";
|
||||
import { formatBalance } from "@app/utils";
|
||||
import * as session from "@app/session";
|
||||
import { State, state } from "./state";
|
||||
import type { Config } from "@app/config";
|
||||
|
||||
import { ethers } from "ethers";
|
||||
import { assert } from "@app/error";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import * as session from "@app/session";
|
||||
import * as utils from "@app/utils";
|
||||
|
||||
export interface VestingInfo {
|
||||
token: string;
|
||||
@@ -14,6 +16,10 @@ export interface VestingInfo {
|
||||
withdrawn: string;
|
||||
}
|
||||
|
||||
export const state = writable<
|
||||
"idle" | "loading" | "withdrawingSign" | "withdrawing" | "withdrawn"
|
||||
>("idle");
|
||||
|
||||
export async function withdrawVested(
|
||||
address: string,
|
||||
config: Config,
|
||||
@@ -27,14 +33,14 @@ export async function withdrawVested(
|
||||
);
|
||||
const signer = config.signer;
|
||||
|
||||
state.set(State.WithdrawingSign);
|
||||
state.set("withdrawingSign");
|
||||
|
||||
const tx = await contract.connect(signer).withdrawVested();
|
||||
|
||||
state.set(State.Withdrawing);
|
||||
state.set("withdrawing");
|
||||
await tx.wait();
|
||||
session.state.refreshBalance(config);
|
||||
state.set(State.Withdrawn);
|
||||
state.set("withdrawn");
|
||||
}
|
||||
|
||||
export async function getInfo(
|
||||
@@ -63,8 +69,8 @@ export async function getInfo(
|
||||
token: token,
|
||||
symbol: symbol,
|
||||
beneficiary: beneficiary,
|
||||
totalVesting: formatBalance(total),
|
||||
withdrawableBalance: formatBalance(withdrawable),
|
||||
withdrawn: formatBalance(withdrawn),
|
||||
totalVesting: utils.formatBalance(total),
|
||||
withdrawableBalance: utils.formatBalance(withdrawable),
|
||||
withdrawn: utils.formatBalance(withdrawn),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,72 +1,86 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Modal from "@app/Modal.svelte";
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Org } from "@app/base/orgs/Org";
|
||||
import type { Config } from "@app/config";
|
||||
import { formatAddress, isAddress } from "@app/utils";
|
||||
import Loading from "@app/Loading.svelte";
|
||||
import { assert } from "@app/error";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import * as utils from "@app/utils";
|
||||
import Address from "@app/Address.svelte";
|
||||
import Button from "@app/Button.svelte";
|
||||
import Loading from "@app/Loading.svelte";
|
||||
import Modal from "@app/Modal.svelte";
|
||||
import TextInput from "@app/TextInput.svelte";
|
||||
|
||||
import type { Org } from "@app/base/orgs/Org";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
import { formatAddress, isAddress } from "@app/utils";
|
||||
|
||||
export let org: Org;
|
||||
export let config: Config;
|
||||
|
||||
enum State {
|
||||
Idle,
|
||||
const dispatch = createEventDispatcher<{ close: boolean }>();
|
||||
|
||||
// Single sig states.
|
||||
Signing,
|
||||
Pending,
|
||||
Success,
|
||||
let state:
|
||||
| "idle"
|
||||
// Single sig.
|
||||
| "signing"
|
||||
| "pending"
|
||||
| "success"
|
||||
// Multi sig.
|
||||
| "proposing"
|
||||
| "proposed"
|
||||
| "failed" = "idle";
|
||||
|
||||
// Multi sig states.
|
||||
Proposing,
|
||||
Proposed,
|
||||
|
||||
Failed,
|
||||
}
|
||||
|
||||
let newOwner: string | undefined = undefined;
|
||||
let state = State.Idle;
|
||||
let error: string | null = null;
|
||||
|
||||
const resetForm = () => {
|
||||
state = State.Idle;
|
||||
};
|
||||
let newOwner: string = "";
|
||||
let errorMessage: string | null = null;
|
||||
let valid: boolean = false;
|
||||
let validationMessage: string | undefined = undefined;
|
||||
|
||||
const onSubmit = async () => {
|
||||
assert(newOwner);
|
||||
|
||||
if (!isAddress(newOwner)) {
|
||||
state = State.Failed;
|
||||
error = `"${newOwner}" is not a valid Ethereum address.`;
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (org && (await utils.isSafe(org.owner, config))) {
|
||||
state = State.Proposing;
|
||||
state = "proposing";
|
||||
await org.setOwnerMultisig(newOwner, config);
|
||||
state = State.Proposed;
|
||||
state = "proposed";
|
||||
} else {
|
||||
state = State.Signing;
|
||||
state = "signing";
|
||||
const tx = await org.setOwner(newOwner, config);
|
||||
state = State.Pending;
|
||||
state = "pending";
|
||||
await tx.wait();
|
||||
state = State.Success;
|
||||
state = "success";
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
state = State.Failed;
|
||||
error = e.message;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
} else {
|
||||
errorMessage = "Unknown error.";
|
||||
}
|
||||
console.error(error);
|
||||
state = "failed";
|
||||
}
|
||||
};
|
||||
|
||||
function validate(address: string) {
|
||||
if (address === "") {
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
if (state !== "idle") {
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
if (!isAddress(address)) {
|
||||
return {
|
||||
valid: false,
|
||||
validationMessage: "Please enter a valid Ethereum address.",
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
$: ({ valid, validationMessage } = validate(newOwner));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -74,10 +88,16 @@
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.message {
|
||||
padding-left: 1rem;
|
||||
padding-top: 1rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if state === State.Success && newOwner}
|
||||
{#if state === "success" && newOwner}
|
||||
<Modal floating small>
|
||||
<div slot="title">✅</div>
|
||||
|
||||
@@ -96,7 +116,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
{:else if state === State.Proposed && org}
|
||||
{:else if state === "proposed" && org}
|
||||
<Modal floating>
|
||||
<div slot="title">🪴</div>
|
||||
|
||||
@@ -118,63 +138,80 @@
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
{:else}
|
||||
<Modal floating error={state === State.Failed} small={state === State.Failed}>
|
||||
{:else if state === "failed"}
|
||||
<Modal floating error small>
|
||||
<div slot="title">
|
||||
🔑
|
||||
<div>Transfer ownership</div>
|
||||
</div>
|
||||
|
||||
<div slot="subtitle">
|
||||
{#if state === State.Signing}
|
||||
<div class="message">
|
||||
{errorMessage}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
<Button
|
||||
variant="negative"
|
||||
on:click={() => {
|
||||
state = "idle";
|
||||
}}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
{:else}
|
||||
<Modal floating>
|
||||
<div slot="title">
|
||||
🔑
|
||||
<div>Transfer ownership</div>
|
||||
</div>
|
||||
|
||||
<div slot="subtitle">
|
||||
{#if state === "signing"}
|
||||
Please confirm the transaction in your wallet.
|
||||
{:else if state === State.Pending}
|
||||
{:else if state === "pending"}
|
||||
Waiting for transaction to be processed…
|
||||
{:else if state === State.Proposing && org}
|
||||
{:else if state === "proposing" && org}
|
||||
Proposal is being submitted to the safe
|
||||
<span class="txt-bold">{formatAddress(org.owner)}</span>
|
||||
, please sign the transaction in your wallet.
|
||||
{:else if state === State.Idle}
|
||||
{:else if state === "idle"}
|
||||
Transfer the ownership of Org <span class="txt-bold">
|
||||
{formatAddress(org.address)}
|
||||
</span>
|
||||
to a new address.
|
||||
{:else if state === State.Failed}
|
||||
<div class="error">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div slot="body" style="display: flex;justify-content: center;">
|
||||
{#if state === State.Idle}
|
||||
<div style="width: 25rem;">
|
||||
<TextInput
|
||||
autofocus
|
||||
disabled={state !== State.Idle}
|
||||
bind:value={newOwner} />
|
||||
{#if state === "idle"}
|
||||
<div style="position: absolute; text-align: left;">
|
||||
<div style="width: 31rem;">
|
||||
<TextInput
|
||||
autofocus
|
||||
{valid}
|
||||
{validationMessage}
|
||||
on:submit={onSubmit}
|
||||
disabled={state !== "idle"}
|
||||
bind:value={newOwner} />
|
||||
</div>
|
||||
</div>
|
||||
{:else if state === State.Pending || state === State.Proposing || state === State.Signing}
|
||||
{:else if state === "pending" || state === "proposing" || state === "signing"}
|
||||
<Loading small center />
|
||||
{:else if state === State.Failed}
|
||||
<!-- ... -->
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div slot="actions" class="actions">
|
||||
{#if state === State.Signing}
|
||||
{#if state === "signing"}
|
||||
<Button variant="text" on:click={() => dispatch("close")}>
|
||||
Cancel
|
||||
</Button>
|
||||
{:else if state === State.Pending}
|
||||
{:else if state === "pending"}
|
||||
<Button variant="text" on:click={() => dispatch("close")}>Close</Button>
|
||||
{:else if state === State.Failed}
|
||||
<Button variant="negative" on:click={resetForm}>Back</Button>
|
||||
{:else}
|
||||
<Button
|
||||
variant="primary"
|
||||
on:click={onSubmit}
|
||||
disabled={!newOwner || state !== State.Idle}>
|
||||
<Button variant="primary" on:click={onSubmit} disabled={!valid}>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
|
||||
+21
-16
@@ -6,12 +6,12 @@
|
||||
import { formatAddress, isAddressEqual } from "@app/utils";
|
||||
import { Org } from "@app/base/orgs/Org";
|
||||
import type { User } from "@app/base/users/User";
|
||||
import Loading from "@app/Loading.svelte";
|
||||
import Error from "@app/Error.svelte";
|
||||
import ErrorModal from "@app/Error.svelte";
|
||||
import Address from "@app/Address.svelte";
|
||||
import * as utils from "@app/utils";
|
||||
import Button from "@app/Button.svelte";
|
||||
import TextInput from "@app/TextInput.svelte";
|
||||
import Loading from "@app/Loading.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
let error: string | null = null;
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
state = State.Checking;
|
||||
|
||||
const domain = `${name}.${config.registrar.domain}`;
|
||||
@@ -62,15 +65,21 @@
|
||||
await tx.wait();
|
||||
state = State.Success;
|
||||
}
|
||||
} catch (e: any) {
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
state = State.Failed;
|
||||
error = e.message;
|
||||
if (e instanceof Error) {
|
||||
error = e.message;
|
||||
} else {
|
||||
error = "Unknown error. Check dev console for details.";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state = State.Mismatch;
|
||||
}
|
||||
};
|
||||
|
||||
$: valid = name !== "" && state === State.Idle;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -120,7 +129,7 @@
|
||||
</div>
|
||||
</Modal>
|
||||
{:else if state === State.Mismatch}
|
||||
<Error floating title="🧣" on:close>
|
||||
<ErrorModal floating title="🧣" on:close>
|
||||
The name <span class="txt-bold">{name}.{config.registrar.domain}</span>
|
||||
does not resolve to
|
||||
<span class="txt-bold">{entity.address}</span>
|
||||
@@ -137,9 +146,9 @@
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</Error>
|
||||
</ErrorModal>
|
||||
{:else if state === State.Failed && error}
|
||||
<Error floating title="Transaction failed" message={error} on:close />
|
||||
<ErrorModal floating title="Transaction failed" message={error} on:close />
|
||||
{:else}
|
||||
<Modal floating>
|
||||
<div slot="title">
|
||||
@@ -172,6 +181,9 @@
|
||||
<TextInput
|
||||
autofocus
|
||||
disabled={state !== State.Idle}
|
||||
on:submit={onSubmit}
|
||||
loading={state === State.Checking}
|
||||
{valid}
|
||||
bind:value={name}>
|
||||
<svelte:fragment slot="right">
|
||||
.{config.registrar.domain}
|
||||
@@ -193,15 +205,8 @@
|
||||
Close
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
variant="primary"
|
||||
on:click={onSubmit}
|
||||
disabled={!name || state !== State.Idle}>
|
||||
{#if state === State.Checking}
|
||||
Checking…
|
||||
{:else}
|
||||
Submit
|
||||
{/if}
|
||||
<Button variant="primary" on:click={onSubmit} disabled={!valid}>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
<Button variant="text" on:click={() => dispatch("close")}>
|
||||
|
||||
Reference in New Issue
Block a user