Skip to content

Commit

Permalink
Merge pull request #38 from eseiker/feature/improve-ux
Browse files Browse the repository at this point in the history
Dropdown UI & Error toast for Web UI
  • Loading branch information
eseiker committed Aug 2, 2023
2 parents 77f535f + 50fdbbf commit 461b438
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 41 deletions.
4 changes: 2 additions & 2 deletions web/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const Modal = forwardRef(
<h3 class="font-bold text-lg">{title}</h3>
{children}
</form>
<form method="dialog" class="modal-backdrop">
<button>close</button>
<form method="dialog" class="modal-backdrop bg-black opacity-30">
<button aria-label="close" />
</form>
</dialog>
);
Expand Down
32 changes: 32 additions & 0 deletions web/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useEffect, useRef } from "preact/hooks";

export interface ToastProps {
text: string;
type?: "info" | "success" | "warning" | "error";
timeoutMs?: number;
}

export const Toast = ({ type, text, timeoutMs = 3000 }: ToastProps) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const timeoutRef = useRef<number | null>();
const alertClass = type ? `alert-${type}` : "";

useEffect(() => {
dialogRef.current?.show();
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
dialogRef.current?.close();
}, timeoutMs);
});

return (
<dialog ref={dialogRef} class="modal" style="width: 0; height: 0;">
<form method="dialog" class="toast">
<div class={`alert ${alertClass} inline`}>
<span>{text}</span>
<button class="btn btn-sm btn-ghost btn-circle"></button>
</div>
</form>
</dialog>
);
};
2 changes: 2 additions & 0 deletions web/fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as $$0 from "./islands/ListAbi.tsx";
import * as $$1 from "./islands/ListSources.tsx";
import * as $$2 from "./islands/ListWebhook.tsx";
import * as $$3 from "./islands/Login.tsx";
import * as $$4 from "./islands/SearchDropdown.tsx";

const manifest = {
routes: {
Expand All @@ -39,6 +40,7 @@ const manifest = {
"./islands/ListSources.tsx": $$1,
"./islands/ListWebhook.tsx": $$2,
"./islands/Login.tsx": $$3,
"./islands/SearchDropdown.tsx": $$4,
},
baseUrl: import.meta.url,
};
Expand Down
31 changes: 25 additions & 6 deletions web/islands/ListAbi.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useCallback, useRef } from "preact/hooks";
import { useCallback, useRef, useState } from "preact/hooks";

import { AbiTable } from "~/components/AbiTable.tsx";
import {
CollapsibleTable,
CollapsibleTableRow,
} from "~/components/CollapsibleTable.tsx";
import { Modal } from "~/components/Modal.tsx";
import { Toast, type ToastProps } from "~/components/Toast.tsx";

import type { AbiEvent } from "https://esm.sh/[email protected]";

Expand All @@ -30,40 +31,57 @@ interface Props {
}

export const ListAbi = ({ entries }: Props) => {
const modalRef = useRef<HTMLDialogElement>(null);
const [toast, setToast] = useState<ToastProps | null>(null);

const handleSubmit = useCallback(async (e: Event) => {
e.preventDefault();

const formData = new FormData(e.target as HTMLFormElement);
const abiJson = formData.get("abiJson");

await fetch(`/api/abi`, {
const res = await fetch(`/api/abi`, {
method: "POST",
body: abiJson,
});

if (!res.ok) {
setToast({ type: "error", text: "Failed to register ABI entries." });
return;
}

if ((await res.json()).length === 0) {
setToast({ type: "info", text: "No new ABI entries to register." });
modalRef.current?.close();
return;
}

location.reload();
}, []);

const handleDelete = useCallback(
(hash: string) => async (e: Event) => {
e.preventDefault();

await fetch(`/api/abi`, {
const res = await fetch(`/api/abi`, {
method: "DELETE",
body: JSON.stringify({ hash }),
});

if (!res.ok) {
setToast({ type: "error", text: "Failed to delete an ABI entry." });
return;
}

location.reload();
},
[],
);

const modalRef = useRef<HTMLDialogElement>(null);

return (
<>
<div class="float-right pb-4">
<button class="btn" onClick={() => modalRef.current?.showModal()}>
<button class="btn" onClick={() => modalRef.current?.show()}>
+
</button>
<Modal title="Register ABI" ref={modalRef}>
Expand All @@ -80,6 +98,7 @@ export const ListAbi = ({ entries }: Props) => {
<input type="submit" class="btn" />
</form>
</Modal>
{toast && <Toast {...toast} />}
</div>
<CollapsibleTable headers={["Hash", "Signature"]}>
{entries.map((entry) => (
Expand Down
60 changes: 48 additions & 12 deletions web/islands/ListSources.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useCallback, useRef } from "preact/hooks";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";

import {
CollapsibleTable,
CollapsibleTableRow,
} from "~/components/CollapsibleTable.tsx";
import { Modal } from "~/components/Modal.tsx";
import { Toast, type ToastProps } from "~/components/Toast.tsx";
import { SearchDropdown } from "~/islands/SearchDropdown.tsx";
import { AbiEntry } from "~/islands/ListAbi.tsx";

export interface SourceEntry {
address: string;
Expand All @@ -17,51 +20,84 @@ interface ListSourcesProps {
}

export const ListSources = ({ entries }: ListSourcesProps) => {
const modalRef = useRef<HTMLDialogElement>(null);
const [toast, setToast] = useState<ToastProps | null>(null);
const [abis, setAbis] = useState<AbiEntry[]>([]);

useEffect(() => {
const getAbis = async () => {
const res = await fetch("/api/abi");
setAbis(await res.json());
};

getAbis();
}, []);

const handleSubmit = useCallback(async (e: Event) => {
e.preventDefault();

const formData = new FormData(e.target as HTMLFormElement);

await fetch("/api/sources", {
const res = await fetch("/api/sources", {
method: "POST",
body: JSON.stringify(Object.fromEntries(formData.entries())),
});

if (!res.ok) {
setToast({
type: "error",
text: "Failed to register a event source entry.",
});
return;
}

location.reload();
}, []);

const handleDelete = useCallback(
(address: string, abiHash: string) => async (e: Event) => {
e.preventDefault();

await fetch(`/api/sources`, {
const res = await fetch(`/api/sources`, {
method: "DELETE",
body: JSON.stringify({ address, abiHash }),
});

if (!res.ok) {
setToast({
type: "error",
text: "Failed to delete an event source entry.",
});
return;
}

location.reload();
},
[],
);

const handleWebhookTest = useCallback(
(address: string, abiHash: string) => (e: Event) => {
(address: string, abiHash: string) => async (e: Event) => {
e.preventDefault();

fetch(`/api/sources/testWebhook`, {
const res = await fetch(`/api/sources/testWebhook`, {
method: "POST",
body: JSON.stringify({ address, abiHash }),
});

if (!res.ok) {
setToast({ type: "error", text: "Failed to trigger test webhook." });
}

setToast({ type: "success", text: "Test webhook triggered." });
},
[],
);

const modalRef = useRef<HTMLDialogElement>(null);

return (
<>
<div class="float-right pb-4">
<button class="btn" onClick={() => modalRef.current?.showModal()}>
<button class="btn" onClick={() => modalRef.current?.show()}>
+
</button>
<Modal title="Register Event Source" ref={modalRef}>
Expand All @@ -78,15 +114,15 @@ export const ListSources = ({ entries }: ListSourcesProps) => {
<label class="label">
<span class="label-text">ABI Hash</span>
</label>
<input
type="text"
<SearchDropdown
name="abiHash"
required
class="input input-bordered w-full max-w-xs"
list={abis}
entrySelector={(e) => e.hash}
/>
<input type="submit" class="btn" />
</form>
</Modal>
{toast && <Toast {...toast} />}
</div>

<CollapsibleTable headers={["Contract Address", "ABI Hash"]}>
Expand Down
Loading

0 comments on commit 461b438

Please sign in to comment.