gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 07/20: more ui: business and admin


From: gnunet
Subject: [taler-wallet-core] 07/20: more ui: business and admin
Date: Mon, 25 Sep 2023 19:51:11 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit 0b7bbed99d155ba030a1328e357ab6751bdbb835
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Sep 21 13:10:16 2023 -0300

    more ui: business and admin
---
 packages/demobank-ui/src/components/Routing.tsx    |  17 +-
 .../src/components/ShowInputErrorLabel.tsx         |   4 +-
 .../demobank-ui/src/pages/AccountPage/index.ts     |   5 +-
 .../demobank-ui/src/pages/AccountPage/state.ts     |   7 +-
 .../demobank-ui/src/pages/AccountPage/views.tsx    |  29 +-
 packages/demobank-ui/src/pages/BankFrame.tsx       |  32 +-
 packages/demobank-ui/src/pages/HomePage.tsx        |   6 +-
 packages/demobank-ui/src/pages/PaymentOptions.tsx  | 131 +++---
 .../src/pages/PaytoWireTransferForm.tsx            |   6 +-
 .../demobank-ui/src/pages/RegistrationPage.tsx     |   2 +-
 .../demobank-ui/src/pages/ShowAccountDetails.tsx   | 278 ++++++------
 .../src/pages/UpdateAccountPassword.tsx            | 278 +++++++-----
 packages/demobank-ui/src/pages/admin/Account.tsx   |  72 ++-
 .../demobank-ui/src/pages/admin/AccountForm.tsx    | 494 +++++++++++++--------
 .../demobank-ui/src/pages/admin/AccountList.tsx    | 182 +++++---
 .../src/pages/admin/CreateNewAccount.tsx           | 174 ++++----
 packages/demobank-ui/src/pages/admin/Home.tsx      |  51 +--
 .../demobank-ui/src/pages/admin/RemoveAccount.tsx  | 302 +++++++++----
 packages/demobank-ui/src/pages/business/Home.tsx   |   2 +-
 19 files changed, 1181 insertions(+), 891 deletions(-)

diff --git a/packages/demobank-ui/src/components/Routing.tsx 
b/packages/demobank-ui/src/components/Routing.tsx
index ef11af76e..b8e39948b 100644
--- a/packages/demobank-ui/src/components/Routing.tsx
+++ b/packages/demobank-ui/src/components/Routing.tsx
@@ -33,12 +33,7 @@ export function Routing(): VNode {
   const backend = useBackendContext();
 
   if (backend.state.status === "loggedOut") {
-    return <BankFrame
-      account={undefined}
-      goToBusinessAccount={() => {
-        route("/business");
-      }}
-    >
+    return <BankFrame >
       <Router history={history}>
         <Route
           path="/login"
@@ -67,12 +62,7 @@ export function Routing(): VNode {
   const { isUserAdministrator, username } = backend.state
 
   return (
-    <BankFrame
-      account={backend.state.username}
-      goToBusinessAccount={() => {
-        route("/business");
-      }}
-    >
+    <BankFrame account={backend.state.username}>
       <Router history={history}>
         <Route
           path="/test"
@@ -121,6 +111,9 @@ export function Routing(): VNode {
                 onPendingOperationFound={(wopid) => {
                   route(`/operation/${wopid}`);
                 }}
+                goToBusinessAccount={() => {
+                  route("/business");
+                }}
                 onRegister={() => {
                   route("/register");
                 }}
diff --git a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx 
b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
index dacffe20a..c5840cad9 100644
--- a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
+++ b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
@@ -24,6 +24,6 @@ export function ShowInputErrorLabel({
   isDirty: boolean;
 }): VNode {
   if (message && isDirty)
-    return <div style={{ marginTop: 8, color: "red" }}>{message}</div>;
-  return <Fragment />;
+    return <div class="text-base" style={{ color: "red" }}>{message}</div>;
+  return <div class="text-base" style={{ }}> </div>;
 }
diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts 
b/packages/demobank-ui/src/pages/AccountPage/index.ts
index ed6945f84..128a6d30f 100644
--- a/packages/demobank-ui/src/pages/AccountPage/index.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/index.ts
@@ -28,6 +28,7 @@ export interface Props {
   onLoadNotOk: <T>(
     error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
   ) => VNode;
+  goToBusinessAccount: () => void;
 }
 
 export type State = State.Loading | State.LoadingError | State.Ready | 
State.InvalidIban | State.UserNotFound;
@@ -51,10 +52,8 @@ export namespace State {
     status: "ready";
     error: undefined;
     account: string, 
-    payto: PaytoUriIBAN | PaytoUriTalerBank, 
-    balance: AmountJson, 
-    balanceIsDebit: boolean, 
     limit: AmountJson,
+    goToBusinessAccount: () => void;
   }
 
   export interface InvalidIban {
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts 
b/packages/demobank-ui/src/pages/AccountPage/state.ts
index 2249b743e..a57e19901 100644
--- a/packages/demobank-ui/src/pages/AccountPage/state.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/state.ts
@@ -20,7 +20,7 @@ import { useBackendContext } from "../../context/backend.js";
 import { useAccountDetails } from "../../hooks/access.js";
 import { Props, State } from "./index.js";
 
-export function useComponentState({ account, onLoadNotOk }: Props): State {
+export function useComponentState({ account, goToBusinessAccount }: Props): 
State {
   const result = useAccountDetails(account);
   const backend = useBackendContext();
   const { i18n } = useTranslationContext();
@@ -60,7 +60,6 @@ export function useComponentState({ account, onLoadNotOk }: 
Props): State {
   const payto = parsePaytoUri(data.paytoUri);
 
   if (!payto || !payto.isKnown || (payto.targetType !== "iban" && 
payto.targetType !== "x-taler-bank")) {
-    console.log(payto)
     return {
       status: "invalid-iban",
       error: result
@@ -75,11 +74,9 @@ export function useComponentState({ account, onLoadNotOk }: 
Props): State {
 
   return {
     status: "ready",
+    goToBusinessAccount,
     error: undefined,
     account,
-    balance,
-    balanceIsDebit,
     limit,
-    payto
   };
 }
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx 
b/packages/demobank-ui/src/pages/AccountPage/views.tsx
index f2cbbba6c..abd14848f 100644
--- a/packages/demobank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -22,6 +22,7 @@ import { PaymentOptions } from "../PaymentOptions.js";
 import { State } from "./index.js";
 import { CopyButton } from "../../components/CopyButton.js";
 import { bankUiSettings } from "../../settings.js";
+import { useBusinessAccountDetails } from "../../hooks/circuit.js";
 
 export function InvalidIbanView({ error }: State.InvalidIban) {
   return (
@@ -77,11 +78,35 @@ function ImportantMessage(): VNode {
 
 }
 
-export function ReadyView({ account, balance, balanceIsDebit, limit, payto }: 
State.Ready): VNode<{}> {
-  const { i18n } = useTranslationContext();
+export function ReadyView({ account, limit, goToBusinessAccount }: 
State.Ready): VNode<{}> {
   return <Fragment>
+    <MaybeBusinessButton account={account} onClick={goToBusinessAccount} />
     <PaymentOptions limit={limit} />
     <Transactions account={account} />
   </Fragment>;
 }
 
+function MaybeBusinessButton({
+  account,
+  onClick,
+}: {
+  account: string;
+  onClick: () => void;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const result = useBusinessAccountDetails(account);
+  if (!result.ok) return <Fragment />;
+  return (
+    <div class="w-full flex justify-end">
+      <button
+        class="disabled:opacity-50 disabled:cursor-default cursor-pointer 
rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm 
hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 
focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+        onClick={(e) => {
+          e.preventDefault()
+          onClick()
+        }}
+      >
+        <i18n.Translate>Business Profile</i18n.Translate>
+      </button>
+    </div>
+  );
+}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx 
b/packages/demobank-ui/src/pages/BankFrame.tsx
index 80a8224d4..4b23686d6 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -39,36 +39,12 @@ const versionText = VERSION
 
 const logger = new Logger("BankFrame");
 
-function MaybeBusinessButton({
-  account,
-  onClick,
-}: {
-  account: string;
-  onClick: () => void;
-}): VNode {
-  const { i18n } = useTranslationContext();
-  const result = useBusinessAccountDetails(account);
-  if (!result.ok) return <Fragment />;
-  return (
-    <a
-      href="#"
-      class="pure-button pure-button-primary"
-      onClick={(e) => {
-        e.preventDefault();
-        onClick();
-      }}
-    >{i18n.str`Business Profile`}</a>
-  );
-}
-
 export function BankFrame({
   children,
-  goToBusinessAccount,
   account,
 }: {
-  account: string | undefined,
+  account?: string,
   children: ComponentChildren;
-  goToBusinessAccount?: () => void;
 }): VNode {
   const { i18n } = useTranslationContext();
   const backend = useBackendContext();
@@ -489,5 +465,9 @@ function AccountBalance({ account }: { account: string }): 
VNode {
   const result = useAccountDetails(account);
   if (!result.ok) return <div />
 
-  return <div>{result.data.balance.credit_debit_indicator === "debit" ? "-" : 
""} {Amounts.currencyOf(result.data.balance.amount)} 
{Amounts.stringifyValue(result.data.balance.amount)}</div>
+  return <div>
+    {Amounts.currencyOf(result.data.balance.amount)}
+    &nbsp;{result.data.balance.credit_debit_indicator === "debit" ? "-" : ""}
+    {Amounts.stringifyValue(result.data.balance.amount)}
+    </div>
 }
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx 
b/packages/demobank-ui/src/pages/HomePage.tsx
index a911f347c..40cc147a6 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -31,14 +31,11 @@ import {
 } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { Loading } from "../components/Loading.js";
-import { useBackendContext } from "../context/backend.js";
 import { getInitialBackendBaseURL } from "../hooks/backend.js";
 import { useSettings } from "../hooks/settings.js";
 import { AccountPage } from "./AccountPage/index.js";
-import { AdminHome } from "./admin/Home.js";
 import { LoginForm } from "./LoginForm.js";
 import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
-import { error } from "console";
 
 const logger = new Logger("AccountPage");
 
@@ -56,10 +53,12 @@ export function HomePage({
   onRegister,
   account,
   onPendingOperationFound,
+  goToBusinessAccount,
 }: {
   account: string,
   onPendingOperationFound: (id: string) => void;
   onRegister: () => void;
+  goToBusinessAccount: () => void;
 }): VNode {
   const [settings] = useSettings();
   const { i18n } = useTranslationContext();
@@ -72,6 +71,7 @@ export function HomePage({
   return (
     <AccountPage
       account={account}
+      goToBusinessAccount={goToBusinessAccount}
       onLoadNotOk={handleNotOkResult(i18n, onRegister)}
     />
   );
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx 
b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index c82c1b28d..5cb09a5d4 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -33,76 +33,79 @@ export function PaymentOptions({ limit }: { limit: 
AmountJson }): VNode {
   const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | 
undefined>();
   // const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | 
undefined>(undefined);
 
-  return (<fieldset>
-    <legend class="px-4 text-base font-semibold leading-6 text-gray-900">
-      <i18n.Translate>Send money to</i18n.Translate>
-    </legend>
+  return (
+    <fieldset>
+      <legend class="px-4 text-base font-semibold leading-6 text-gray-900">
+        <i18n.Translate>Send money to</i18n.Translate>
+      </legend>
 
-    <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
-      {/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: 
"border-gray-300" --> */}
-      <label class={"relative flex cursor-pointer rounded-lg border bg-white 
p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? 
"border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
-        <input type="radio" name="project-type" value="Newsletter" 
class="sr-only" aria-labelledby="project-type-0-label" 
aria-describedby="project-type-0-description-0 project-type-0-description-1" 
onClick={() => {
-          setTab("charge-wallet")
-        }} />
-        <span class="flex flex-1">
-          <span class="flex flex-col">
-            <span id="project-type-0-label" class="block text-sm font-medium 
text-gray-900">
-              <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
-            </span>
-            <span id="project-type-0-description-0" class="mt-1 flex 
items-center text-sm text-gray-500">
-              <i18n.Translate>Withdraw digital money into your mobile wallet 
or browser extension</i18n.Translate>
+      <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 
sm:gap-x-4">
+        {/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not 
Active: "border-gray-300" --> */}
+        <label class={"relative flex cursor-pointer rounded-lg border bg-white 
p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? 
"border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
+          <input type="radio" name="project-type" value="Newsletter" 
class="sr-only" aria-labelledby="project-type-0-label" 
aria-describedby="project-type-0-description-0 project-type-0-description-1" 
onClick={() => {
+            setTab("charge-wallet")
+          }} />
+          <span class="flex flex-1">
+            <span class="flex flex-col">
+              <span id="project-type-0-label" class="block text-sm font-medium 
text-gray-900">
+                <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
+              </span>
+              <span id="project-type-0-description-0" class="mt-1 flex 
items-center text-sm text-gray-500">
+                <i18n.Translate>Withdraw digital money into your mobile wallet 
or browser extension</i18n.Translate>
+              </span>
             </span>
           </span>
-        </span>
-        <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === 
"charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" 
fill="currentColor" aria-hidden="true">
-          <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 
16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 
1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
-        </svg>
-      </label>
+          <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === 
"charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" 
fill="currentColor" aria-hidden="true">
+            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 
16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 
1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+          </svg>
+        </label>
 
 
-      <label class={"relative flex cursor-pointer rounded-lg border bg-white 
p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? 
"border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
-        <input type="radio" name="project-type" value="Existing Customers" 
class="sr-only" aria-labelledby="project-type-1-label" 
aria-describedby="project-type-1-description-0 project-type-1-description-1" 
onClick={() => {
-          setTab("wire-transfer")
-        }} />
-        <span class="flex flex-1">
-          <span class="flex flex-col">
-            <span id="project-type-1-label" class="block text-sm font-medium 
text-gray-900">
-              <i18n.Translate>another bank account</i18n.Translate>
-            </span>
-            <span id="project-type-1-description-0" class="mt-1 flex 
items-center text-sm text-gray-500">
-              <i18n.Translate>Make a wire transfer to an account which you 
know the address.</i18n.Translate>
+        <label class={"relative flex cursor-pointer rounded-lg border bg-white 
p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? 
"border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
+          <input type="radio" name="project-type" value="Existing Customers" 
class="sr-only" aria-labelledby="project-type-1-label" 
aria-describedby="project-type-1-description-0 project-type-1-description-1" 
onClick={() => {
+            setTab("wire-transfer")
+          }} />
+          <span class="flex flex-1">
+            <span class="flex flex-col">
+              <span id="project-type-1-label" class="block text-sm font-medium 
text-gray-900">
+                <i18n.Translate>another bank account</i18n.Translate>
+              </span>
+              <span id="project-type-1-description-0" class="mt-1 flex 
items-center text-sm text-gray-500">
+                <i18n.Translate>Make a wire transfer to an account which you 
know the address.</i18n.Translate>
+              </span>
             </span>
           </span>
-        </span>
-        <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === 
"wire-transfer" ? "visible" : "hidden" }} viewBox="0 0 20 20" 
fill="currentColor" aria-hidden="true">
-          <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 
16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 
1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
-        </svg>
-      </label>
-    </div>
-    {tab === "charge-wallet" && (
-      <WalletWithdrawForm
-        focus
-        limit={limit}
-        onSuccess={(id) => {
-          updateSettings("currentWithdrawalOperationId", id);
-        }}
-        onCancel={() => {
-          setTab(undefined)
-        }}
-      />
-    )}
-    {tab === "wire-transfer" && (
-      <PaytoWireTransferForm
-        focus
-        limit={limit}
-        onSuccess={() => {
-          notifyInfo(i18n.str`Wire transfer created!`);
-        }}
-        onCancel={() => {
-          setTab(undefined)
-        }}
-      />
-    )}
+          <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === 
"wire-transfer" ? "visible" : "hidden" }} viewBox="0 0 20 20" 
fill="currentColor" aria-hidden="true">
+            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 
16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 
1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+          </svg>
+        </label>
+      </div>
+      {tab === "charge-wallet" && (
+        <WalletWithdrawForm
+          focus
+          limit={limit}
+          onSuccess={(id) => {
+            updateSettings("currentWithdrawalOperationId", id);
+          }}
+          onCancel={() => {
+            setTab(undefined)
+          }}
+        />
+      )}
+      {tab === "wire-transfer" && (
+        <PaytoWireTransferForm
+          focus
+          title={i18n.str`Transfer details`}
+          limit={limit}
+          onSuccess={() => {
+            notifyInfo(i18n.str`Wire transfer created!`);
+          }}
+          onCancel={() => {
+            setTab(undefined)
+          }}
+        />
+      )}
 
-  </fieldset>)
+    </fieldset>
+  )
 }
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index cdaf363e3..af6f7caee 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -44,10 +44,12 @@ const logger = new Logger("PaytoWireTransferForm");
 
 export function PaytoWireTransferForm({
   focus,
+  title,
   onSuccess,
   onCancel,
   limit,
 }: {
+  title: TranslatedString,
   focus?: boolean;
   onSuccess: () => void;
   onCancel: (() => void) | undefined;
@@ -158,7 +160,9 @@ export function PaytoWireTransferForm({
 
   return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 
bg-gray-100 my-4 px-4 pb-4 rounded-lg">
     <div class="px-4 sm:px-0">
-      <h2 class="text-base font-semibold leading-7 
text-gray-900"><i18n.Translate>Transfer details</i18n.Translate></h2>
+      <h2 class="text-base font-semibold leading-7 text-gray-900">
+        {title}
+      </h2>
       <div>
         <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 
sm:gap-x-4">
           <label class={"relative flex cursor-pointer rounded-lg border 
bg-white p-4 shadow-sm focus:outline-none" + (!isRawPayto ? "border-indigo-600 
ring-2 ring-indigo-600" : "border-gray-300")}>
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx 
b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index b912b9060..f972e0380 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -45,7 +45,7 @@ export function RegistrationPage({
   return <RegistrationForm onComplete={onComplete} />;
 }
 
-export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
+export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/;
 
 /**
  * Collect and submit registration data.
diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx 
b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
index 91b50b84c..6acf0361e 100644
--- a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
@@ -1,5 +1,5 @@
 import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, 
useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode,h } from "preact";
+import { VNode, h } from "preact";
 import { useAdminAccountAPI, useBusinessAccountDetails } from 
"../hooks/circuit.js";
 import { useState } from "preact/hooks";
 import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
@@ -7,137 +7,161 @@ import { buildRequestErrorMessage } from "../utils.js";
 import { AccountForm } from "./admin/AccountForm.js";
 
 export function ShowAccountDetails({
-    account,
-    onClear,
-    onUpdateSuccess,
-    onLoadNotOk,
-    onChangePassword,
-  }: {
-    onLoadNotOk: <T>(
-      error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
-    ) => VNode;
-    onClear?: () => void;
-    onChangePassword: () => void;
-    onUpdateSuccess: () => void;
-    account: string;
-  }): VNode {
-    const { i18n } = useTranslationContext();
-    const result = useBusinessAccountDetails(account);
-    const { updateAccount } = useAdminAccountAPI();
-    const [update, setUpdate] = useState(false);
-    const [submitAccount, setSubmitAccount] = useState<
-      SandboxBackend.Circuit.CircuitAccountData | undefined
-    >();
-  
-    if (!result.ok) {
-      if (result.loading || result.type === ErrorType.TIMEOUT) {
-        return onLoadNotOk(result);
-      }
-      if (result.status === HttpStatusCode.NotFound) {
-        return <div>account not found</div>;
-      }
+  account,
+  onClear,
+  onUpdateSuccess,
+  onLoadNotOk,
+  onChangePassword,
+}: {
+  onLoadNotOk: <T>(
+    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+  ) => VNode;
+  onClear?: () => void;
+  onChangePassword: () => void;
+  onUpdateSuccess: () => void;
+  account: string;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const result = useBusinessAccountDetails(account);
+  const { updateAccount } = useAdminAccountAPI();
+  const [update, setUpdate] = useState(false);
+  const [submitAccount, setSubmitAccount] = useState<
+    SandboxBackend.Circuit.CircuitAccountData | undefined
+  >();
+
+  if (!result.ok) {
+    if (result.loading || result.type === ErrorType.TIMEOUT) {
       return onLoadNotOk(result);
     }
-  
-    return (
-      <div>
-        <div>
-          <h1 class="nav welcome-text">
-            <i18n.Translate>Business account details</i18n.Translate>
-          </h1>
+    if (result.status === HttpStatusCode.NotFound) {
+      return <div>account not found</div>;
+    }
+    return onLoadNotOk(result);
+  }
+
+  async function doUpdate() {
+    if (!update) {
+      setUpdate(true);
+    } else {
+      if (!submitAccount) return;
+      try {
+        await updateAccount(account, {
+          cashout_address: submitAccount.cashout_address,
+          contact_data: submitAccount.contact_data,
+        });
+        onUpdateSuccess();
+      } catch (error) {
+        if (error instanceof RequestError) {
+          notify(
+            buildRequestErrorMessage(i18n, error.cause, {
+              onClientError: (status) =>
+                status === HttpStatusCode.Forbidden
+                  ? i18n.str`The rights to change the account are not 
sufficient`
+                  : status === HttpStatusCode.NotFound
+                    ? i18n.str`The username was not found`
+                    : undefined,
+            }),
+          );
+        } else {
+          notifyError(
+            i18n.str`Operation failed, please report`,
+            (error instanceof Error
+              ? error.message
+              : JSON.stringify(error)) as TranslatedString
+          )
+        }
+      }
+    }
+  }
+
+  return (
+    <div>
+      <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 
bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+        <div class="px-4 sm:px-0">
+          <h2 class="text-base font-semibold leading-7 text-gray-900">
+            {update ?
+              <i18n.Translate>Update account</i18n.Translate>
+              :
+              <i18n.Translate>Account details</i18n.Translate>
+            }
+          </h2>
+          <div class="mt-4">
+        <div class="flex items-center justify-between">
+          <span class="flex flex-grow flex-col">
+            <span class="text-sm text-black font-medium leading-6 " 
id="availability-label">
+              <i18n.Translate>change the account details</i18n.Translate>
+            </span>
+          </span>
+          <button type="button" data-enabled={!update} class="bg-indigo-600 
data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 
cursor-pointer rounded-full ring-2 border-gray-600 transition-colors 
duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 
focus:ring-offset-2" role="switch" aria-checked="false" 
aria-labelledby="availability-label" aria-describedby="availability-description"
+            onClick={() => {
+              setUpdate(!update)
+            }}>
+            <span aria-hidden="true" data-enabled={!update} 
class="translate-x-5 data-[enabled=true]:translate-x-0 pointer-events-none 
inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition 
duration-200 ease-in-out"></span>
+          </button>
         </div>
-        <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
-          <AccountForm
-            template={result.data}
-            purpose={update ? "update" : "show"}
-            onChange={(a) => setSubmitAccount(a)}
-          />
-  
-          <p class="buttons-account">
-            <div
-              style={{
-                display: "flex",
-                justifyContent: "space-between",
-                flexFlow: "wrap-reverse",
-              }}
-            >
+      </div>
+
+        </div>
+        <AccountForm
+          template={result.data}
+          purpose={update ? "update" : "show"}
+          onChange={(a) => setSubmitAccount(a)}
+        >
+
+        </AccountForm>
+
+        <p class="buttons-account">
+          <div
+            style={{
+              display: "flex",
+              justifyContent: "space-between",
+              flexFlow: "wrap-reverse",
+            }}
+          >
+            <div>
+              {onClear ? (
+                <input
+                  class="pure-button"
+                  type="submit"
+                  value={i18n.str`Close`}
+                  onClick={async (e) => {
+                    e.preventDefault();
+                    onClear();
+                  }}
+                />
+              ) : undefined}
+            </div>
+            <div style={{ display: "flex" }}>
               <div>
-                {onClear ? (
-                  <input
-                    class="pure-button"
-                    type="submit"
-                    value={i18n.str`Close`}
-                    onClick={async (e) => {
-                      e.preventDefault();
-                      onClear();
-                    }}
-                  />
-                ) : undefined}
+                <input
+                  id="select-exchange"
+                  class="pure-button pure-button-primary content"
+                  disabled={update && !submitAccount}
+                  type="submit"
+                  value={i18n.str`Change password`}
+                  onClick={async (e) => {
+                    e.preventDefault();
+                    onChangePassword();
+                  }}
+                />
               </div>
-              <div style={{ display: "flex" }}>
-                <div>
-                  <input
-                    id="select-exchange"
-                    class="pure-button pure-button-primary content"
-                    disabled={update && !submitAccount}
-                    type="submit"
-                    value={i18n.str`Change password`}
-                    onClick={async (e) => {
-                      e.preventDefault();
-                      onChangePassword();
-                    }}
-                  />
-                </div>
-                <div>
-                  <input
-                    id="select-exchange"
-                    class="pure-button pure-button-primary content"
-                    disabled={update && !submitAccount}
-                    type="submit"
-                    value={update ? i18n.str`Confirm` : i18n.str`Update`}
-                    onClick={async (e) => {
-                      e.preventDefault();
-  
-                      if (!update) {
-                        setUpdate(true);
-                      } else {
-                        if (!submitAccount) return;
-                        try {
-                          await updateAccount(account, {
-                            cashout_address: submitAccount.cashout_address,
-                            contact_data: submitAccount.contact_data,
-                          });
-                          onUpdateSuccess();
-                        } catch (error) {
-                          if (error instanceof RequestError) {
-                            notify(
-                              buildRequestErrorMessage(i18n, error.cause, {
-                                onClientError: (status) =>
-                                  status === HttpStatusCode.Forbidden
-                                    ? i18n.str`The rights to change the 
account are not sufficient`
-                                    : status === HttpStatusCode.NotFound
-                                      ? i18n.str`The username was not found`
-                                      : undefined,
-                              }),
-                            );
-                          } else {
-                            notifyError(
-                              i18n.str`Operation failed, please report`,
-                              (error instanceof Error
-                                ? error.message
-                                : JSON.stringify(error)) as TranslatedString
-                            )
-                          }
-                        }
-                      }
-                    }}
-                  />
-                </div>
+              <div>
+                <input
+                  id="select-exchange"
+                  class="pure-button pure-button-primary content"
+                  disabled={update && !submitAccount}
+                  type="submit"
+                  value={update ? i18n.str`Confirm` : i18n.str`Update`}
+                  onClick={async (e) => {
+                    e.preventDefault();
+                    doUpdate()
+                  }}
+                />
               </div>
             </div>
-          </p>
-        </div>
+          </div>
+        </p>
       </div>
-    );
-  }
-  
\ No newline at end of file
+    </div>
+  );
+}
diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx 
b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
index 084a5b643..d19c411f3 100644
--- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
@@ -1,131 +1,181 @@
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
 import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, 
useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
 import { useAdminAccountAPI, useBusinessAccountDetails } from 
"../hooks/circuit.js";
-import { useState } from "preact/hooks";
-import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
-import { VNode,h ,Fragment} from "preact";
 import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
 
 export function UpdateAccountPassword({
-    account,
-    onClear,
-    onUpdateSuccess,
-    onLoadNotOk,
-  }: {
-    onLoadNotOk: <T>(
-      error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
-    ) => VNode;
-    onClear: () => void;
-    onUpdateSuccess: () => void;
-    account: string;
-  }): VNode {
-    const { i18n } = useTranslationContext();
-    const result = useBusinessAccountDetails(account);
-    const { changePassword } = useAdminAccountAPI();
-    const [password, setPassword] = useState<string | undefined>();
-    const [repeat, setRepeat] = useState<string | undefined>();
-  
-    if (!result.ok) {
-      if (result.loading || result.type === ErrorType.TIMEOUT) {
-        return onLoadNotOk(result);
-      }
-      if (result.status === HttpStatusCode.NotFound) {
-        return <div>account not found</div>;
-      }
+  account,
+  onCancel,
+  onUpdateSuccess,
+  onLoadNotOk,
+  focus,
+}: {
+  onLoadNotOk: <T>(
+    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+  ) => VNode;
+  onCancel: () => void;
+  focus?: boolean,
+  onUpdateSuccess: () => void;
+  account: string;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const result = useBusinessAccountDetails(account);
+  const { changePassword } = useAdminAccountAPI();
+  const [password, setPassword] = useState<string | undefined>();
+  const [repeat, setRepeat] = useState<string | undefined>();
+
+  const ref = useRef<HTMLInputElement>(null);
+  useEffect(() => {
+    if (focus) ref.current?.focus();
+  }, [focus]);
+
+  if (!result.ok) {
+    if (result.loading || result.type === ErrorType.TIMEOUT) {
       return onLoadNotOk(result);
     }
-  
-    const errors = undefinedIfEmpty({
-      password: !password ? i18n.str`required` : undefined,
-      repeat: !repeat
-        ? i18n.str`required`
-        : password !== repeat
-          ? i18n.str`password doesn't match`
-          : undefined,
-    });
-  
-    return (
-      <div>
-        <div>
-          <h1 class="nav welcome-text">
-            <i18n.Translate>Update password for {account}</i18n.Translate>
-          </h1>
-        </div>
-  
-        <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
-          <form class="pure-form">
-            <fieldset>
-              <label>{i18n.str`Password`}</label>
-              <input
-                type="password"
-                value={password ?? ""}
-                onChange={(e) => {
-                  setPassword(e.currentTarget.value);
-                }}
-              />
-              <ShowInputErrorLabel
-                message={errors?.password}
-                isDirty={password !== undefined}
-              />
-            </fieldset>
-            <fieldset>
-              <label>{i18n.str`Repeat password`}</label>
-              <input
-                type="password"
-                value={repeat ?? ""}
-                onChange={(e) => {
-                  setRepeat(e.currentTarget.value);
-                }}
-              />
-              <ShowInputErrorLabel
-                message={errors?.repeat}
-                isDirty={repeat !== undefined}
-              />
-            </fieldset>
-          </form>
-          <p>
-            <div style={{ display: "flex", justifyContent: "space-between" }}>
-              <div>
+    if (result.status === HttpStatusCode.NotFound) {
+      return <div>account not found</div>;
+    }
+    return onLoadNotOk(result);
+  }
+
+  const errors = undefinedIfEmpty({
+    password: !password ? i18n.str`required` : undefined,
+    repeat: !repeat
+      ? i18n.str`required`
+      : password !== repeat
+        ? i18n.str`password doesn't match`
+        : undefined,
+  });
+
+  async function doChangePassword() {
+    if (!!errors || !password) return;
+    try {
+      const r = await changePassword(account, {
+        new_password: password,
+      });
+      onUpdateSuccess();
+    } catch (error) {
+      if (error instanceof RequestError) {
+        notify(buildRequestErrorMessage(i18n, error.cause));
+      } else {
+        notifyError(i18n.str`Operation failed, please report`, (error 
instanceof Error
+          ? error.message
+          : JSON.stringify(error)) as TranslatedString)
+      }
+    }
+  }
+
+  return (
+    <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 
bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+      <div class="px-4 sm:px-0">
+        <h2 class="text-base font-semibold leading-7 text-gray-900">
+          <i18n.Translate>Update password for account 
"{account}"</i18n.Translate>
+        </h2>
+      </div>
+      <form
+        class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl 
md:col-span-2"
+        autoCapitalize="none"
+        autoCorrect="off"
+        onSubmit={e => {
+          e.preventDefault()
+        }}
+      >
+        <div class="px-4 py-6 sm:p-8">
+          <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+
+            <div class="sm:col-span-5">
+              <label
+                class="block text-sm font-medium leading-6 text-gray-900"
+                for="password"
+              >
+                {i18n.str`New password`}
+              </label>
+              <div class="mt-2">
                 <input
-                  class="pure-button"
-                  type="submit"
-                  value={i18n.str`Close`}
-                  onClick={async (e) => {
-                    e.preventDefault();
-                    onClear();
+                  ref={ref}
+                  type="password"
+                  class="block w-full rounded-md border-0 py-1.5 text-gray-900 
shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
+                  name="password"
+                  id="password"
+                  data-error={!!errors?.password && password !== undefined}
+                  value={password ?? ""}
+                  onChange={(e) => {
+                    setPassword(e.currentTarget.value)
                   }}
+                  // placeholder=""
+                  autocomplete="off"
+                />
+                <ShowInputErrorLabel
+                  message={errors?.password}
+                  isDirty={password !== undefined}
                 />
               </div>
-              <div>
+              {/* <p class="mt-2 text-sm text-gray-500" >
+              <i18n.Translate>user </i18n.Translate>
+            </p> */}
+            </div>
+
+            <div class="sm:col-span-5">
+              <label
+                class="block text-sm font-medium leading-6 text-gray-900"
+                for="repeat"
+              >
+                {i18n.str`Type it again`}
+              </label>
+              <div class="mt-2">
                 <input
-                  id="select-exchange"
-                  class="pure-button pure-button-primary content"
-                  disabled={!!errors}
-                  type="submit"
-                  value={i18n.str`Confirm`}
-                  onClick={async (e) => {
-                    e.preventDefault();
-                    if (!!errors || !password) return;
-                    try {
-                      const r = await changePassword(account, {
-                        new_password: password,
-                      });
-                      onUpdateSuccess();
-                    } catch (error) {
-                      if (error instanceof RequestError) {
-                        notify(buildRequestErrorMessage(i18n, error.cause));
-                      } else {
-                        notifyError(i18n.str`Operation failed, please report`, 
(error instanceof Error
-                          ? error.message
-                          : JSON.stringify(error)) as TranslatedString)
-                      }
-                    }
+                  type="password"
+                  class="block w-full rounded-md border-0 py-1.5 text-gray-900 
shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
+                  name="repeat"
+                  id="repeat"
+                  data-error={!!errors?.repeat && repeat !== undefined}
+                  value={repeat ?? ""}
+                  onChange={(e) => {
+                    setRepeat(e.currentTarget.value)
                   }}
+                  // placeholder=""
+                  autocomplete="off"
+                />
+                <ShowInputErrorLabel
+                  message={errors?.repeat}
+                  isDirty={repeat !== undefined}
                 />
               </div>
+              <p class="mt-2 text-sm text-gray-500" >
+                <i18n.Translate>repeat the same password</i18n.Translate>
+              </p>
             </div>
-          </p>
+
+
+
+          </div>
         </div>
-      </div>
-    );
-  }
\ No newline at end of file
+        <div class="flex items-center justify-between gap-x-6 border-t 
border-gray-900/10 px-4 py-4 sm:px-8">
+          {onCancel ?
+            <button type="button" class="text-sm font-semibold leading-6 
text-gray-900"
+              onClick={onCancel}
+            >
+              <i18n.Translate>Cancel</i18n.Translate>
+            </button>
+            : <div />
+          }
+          <button type="submit"
+            class="disabled:opacity-50 disabled:cursor-default cursor-pointer 
rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm 
hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 
focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+            disabled={!!errors}
+            onClick={(e) => {
+              e.preventDefault()
+              doChangePassword()
+            }}
+          >
+            <i18n.Translate>Change</i18n.Translate>
+          </button>
+        </div>
+      </form>
+    </div>
+
+  );
+}
\ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx 
b/packages/demobank-ui/src/pages/admin/Account.tsx
index 8ab3e1323..90ddd611d 100644
--- a/packages/demobank-ui/src/pages/admin/Account.tsx
+++ b/packages/demobank-ui/src/pages/admin/Account.tsx
@@ -7,50 +7,30 @@ import { notifyInfo, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { Fragment, h, VNode } from "preact";
 
 export function AdminAccount({ onRegister }: { onRegister: () => void }): 
VNode {
-    const { i18n } = useTranslationContext();
-    const r = useBackendContext();
-    const account = r.state.status === "loggedIn" ? r.state.username : "admin";
-    const result = useAccountDetails(account);
-  
-    if (!result.ok) {
-      return handleNotOkResult(i18n, onRegister)(result);
-    }
-    const { data } = result;
-    const balance = Amounts.parseOrThrow(data.balance.amount);
-    const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
-    const balanceIsDebit = result.data.balance.credit_debit_indicator == 
"debit";
-    const limit = balanceIsDebit
-      ? Amounts.sub(debitThreshold, balance).amount
-      : Amounts.add(balance, debitThreshold).amount;
-    if (!balance) return <Fragment />;
-    return (
-      <Fragment>
-        <section id="assets">
-          <div class="asset-summary">
-            <h2>{i18n.str`Bank account balance`}</h2>
-            {!balance ? (
-              <div class="large-amount" style={{ color: "gray" }}>
-                Waiting server response...
-              </div>
-            ) : (
-              <div class="large-amount amount">
-                {balanceIsDebit ? <b>-</b> : null}
-                <span 
class="value">{`${Amounts.stringifyValue(balance)}`}</span>
-                &nbsp;
-                <span class="currency">{`${balance.currency}`}</span>
-              </div>
-            )}
-          </div>
-        </section>
-        <PaytoWireTransferForm
-          focus
-          limit={limit}
-          onSuccess={() => {
-            notifyInfo(i18n.str`Wire transfer created!`);
-          }}
-          onCancel={undefined}
-        />
-      </Fragment>
-    );
+  const { i18n } = useTranslationContext();
+  const r = useBackendContext();
+  const account = r.state.status === "loggedIn" ? r.state.username : "admin";
+  const result = useAccountDetails(account);
+
+  if (!result.ok) {
+    return handleNotOkResult(i18n, onRegister)(result);
   }
-  
\ No newline at end of file
+  const { data } = result;
+  const balance = Amounts.parseOrThrow(data.balance.amount);
+  const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
+  const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
+  const limit = balanceIsDebit
+    ? Amounts.sub(debitThreshold, balance).amount
+    : Amounts.add(balance, debitThreshold).amount;
+  if (!balance) return <Fragment />;
+  return (
+    <PaytoWireTransferForm
+      title={i18n.str`Make a wire transfer`}
+      limit={limit}
+      onSuccess={() => {
+        notifyInfo(i18n.str`Wire transfer created!`);
+      }}
+      onCancel={undefined}
+    />
+  );
+}
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx 
b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 9ca0323a1..02df824a2 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -1,9 +1,9 @@
-import { VNode,h  } from "preact";
+import { ComponentChildren, VNode, h } from "preact";
 import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
 import { PartialButDefined, RecursivePartial, WithIntermediate, 
undefinedIfEmpty, validateIBAN } from "../../utils.js";
-import { useState } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
 import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { parsePaytoUri } from "@gnu-taler/taler-util";
+import { buildPayto, parsePaytoUri } from "@gnu-taler/taler-util";
 
 const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
 const EMAIL_REGEX =
@@ -19,201 +19,301 @@ const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
  * @returns
  */
 export function AccountForm({
-    template,
-    purpose,
-    onChange,
-  }: {
-    template: SandboxBackend.Circuit.CircuitAccountData | undefined;
-    onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => 
void;
-    purpose: "create" | "update" | "show";
-  }): VNode {
-    const initial = initializeFromTemplate(template);
-    const [form, setForm] = useState(initial);
-    const [errors, setErrors] = useState<
-      RecursivePartial<typeof initial> | undefined
-    >(undefined);
-    const { i18n } = useTranslationContext();
-  
-    function updateForm(newForm: typeof initial): void {
-      const parsed = !newForm.cashout_address
-        ? undefined
-        : parsePaytoUri(newForm.cashout_address);
-  
-      const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
-        cashout_address: !newForm.cashout_address
+  template,
+  purpose,
+  onChange,
+  focus,
+  children,
+}: {
+  focus?: boolean,
+  children: ComponentChildren,
+  template: SandboxBackend.Circuit.CircuitAccountData | undefined;
+  onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
+  purpose: "create" | "update" | "show";
+}): VNode {
+  const initial = initializeFromTemplate(template);
+  const [form, setForm] = useState(initial);
+  const [errors, setErrors] = useState<
+    RecursivePartial<typeof initial> | undefined
+  >(undefined);
+  const { i18n } = useTranslationContext();
+  const ref = useRef<HTMLInputElement>(null);
+  useEffect(() => {
+    if (focus) ref.current?.focus();
+  }, [focus]);
+
+  function updateForm(newForm: typeof initial): void {
+
+    const parsed = !newForm.cashout_address
+      ? undefined
+      : buildPayto("iban", newForm.cashout_address, undefined);;
+
+    const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
+      cashout_address: !newForm.cashout_address
+        ? i18n.str`required`
+        : !parsed
+          ? i18n.str`does not follow the pattern`
+          : !parsed.isKnown || parsed.targetType !== "iban"
+            ? i18n.str`only "IBAN" target are supported`
+            : !IBAN_REGEX.test(parsed.iban)
+              ? i18n.str`IBAN should have just uppercased letters and numbers`
+              : validateIBAN(parsed.iban, i18n),
+      contact_data: undefinedIfEmpty({
+        email: !newForm.contact_data?.email
           ? i18n.str`required`
-          : !parsed
-            ? i18n.str`does not follow the pattern`
-            : !parsed.isKnown || parsed.targetType !== "iban"
-              ? i18n.str`only "IBAN" target are supported`
-              : !IBAN_REGEX.test(parsed.iban)
-                ? i18n.str`IBAN should have just uppercased letters and 
numbers`
-                : validateIBAN(parsed.iban, i18n),
-        contact_data: undefinedIfEmpty({
-          email: !newForm.contact_data?.email
-            ? i18n.str`required`
-            : !EMAIL_REGEX.test(newForm.contact_data.email)
-              ? i18n.str`it should be an email`
+          : !EMAIL_REGEX.test(newForm.contact_data.email)
+            ? i18n.str`it should be an email`
+            : undefined,
+        phone: !newForm.contact_data?.phone
+          ? i18n.str`required`
+          : !newForm.contact_data.phone.startsWith("+")
+            ? i18n.str`should start with +`
+            : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
+              ? i18n.str`phone number can't have other than numbers`
               : undefined,
-          phone: !newForm.contact_data?.phone
-            ? i18n.str`required`
-            : !newForm.contact_data.phone.startsWith("+")
-              ? i18n.str`should start with +`
-              : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
-                ? i18n.str`phone number can't have other than numbers`
-                : undefined,
-        }),
-        iban: !newForm.iban
-          ? undefined //optional field
-          : !IBAN_REGEX.test(newForm.iban)
-            ? i18n.str`IBAN should have just uppercased letters and numbers`
-            : validateIBAN(newForm.iban, i18n),
-        name: !newForm.name ? i18n.str`required` : undefined,
-        username: !newForm.username ? i18n.str`required` : undefined,
-      });
-      setErrors(errors);
-      setForm(newForm);
-      onChange(errors === undefined ? (newForm as any) : undefined);
-    }
-  
-    return (
-      <form class="pure-form">
-        <fieldset>
-          <label for="username">
-            {i18n.str`Username`}
-            {purpose === "create" && <b style={{ color: "red" }}>*</b>}
-          </label>
-          <input
-            name="username"
-            type="text"
-            disabled={purpose !== "create"}
-            value={form.username}
-            onChange={(e) => {
-              form.username = e.currentTarget.value;
-              updateForm(structuredClone(form));
-            }}
-          />{" "}
-          <ShowInputErrorLabel
-            message={errors?.username}
-            isDirty={form.username !== undefined}
-          />
-        </fieldset>
-        <fieldset>
-          <label>
-            {i18n.str`Name`}
-            {purpose === "create" && <b style={{ color: "red" }}>*</b>}
-          </label>
-          <input
-            disabled={purpose !== "create"}
-            value={form.name ?? ""}
-            onChange={(e) => {
-              form.name = e.currentTarget.value;
-              updateForm(structuredClone(form));
-            }}
-          />
-          <ShowInputErrorLabel
-            message={errors?.name}
-            isDirty={form.name !== undefined}
-          />
-        </fieldset>
-        {purpose !== "create" && (
-          <fieldset>
-            <label>{i18n.str`Internal IBAN`}</label>
-            <input
-              disabled={true}
-              value={form.iban ?? ""}
-              onChange={(e) => {
-                form.iban = e.currentTarget.value;
-                updateForm(structuredClone(form));
-              }}
-            />
-            <ShowInputErrorLabel
-              message={errors?.iban}
-              isDirty={form.iban !== undefined}
-            />
-          </fieldset>
-        )}
-        <fieldset>
-          <label>
-            {i18n.str`Email`}
-            {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
-          </label>
-          <input
-            disabled={purpose === "show"}
-            value={form.contact_data.email ?? ""}
-            onChange={(e) => {
-              form.contact_data.email = e.currentTarget.value;
-              updateForm(structuredClone(form));
-            }}
-          />
-          <ShowInputErrorLabel
-            message={errors?.contact_data?.email}
-            isDirty={form.contact_data.email !== undefined}
-          />
-        </fieldset>
-        <fieldset>
-          <label>
-            {i18n.str`Phone`}
-            {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
-          </label>
-          <input
-            disabled={purpose === "show"}
-            value={form.contact_data.phone ?? ""}
-            onChange={(e) => {
-              form.contact_data.phone = e.currentTarget.value;
-              updateForm(structuredClone(form));
-            }}
-          />
-          <ShowInputErrorLabel
-            message={errors?.contact_data?.phone}
-            isDirty={form.contact_data?.phone !== undefined}
-          />
-        </fieldset>
-        <fieldset>
-          <label>
-            {i18n.str`Cashout address`}
-            {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
-          </label>
-          <input
-            disabled={purpose === "show"}
-            value={(form.cashout_address ?? 
"").substring("payto://iban/".length)}
-            onChange={(e) => {
-              form.cashout_address = "payto://iban/" + e.currentTarget.value;
-              updateForm(structuredClone(form));
-            }}
-          />
-          <ShowInputErrorLabel
-            message={errors?.cashout_address}
-            isDirty={form.cashout_address !== undefined}
-          />
-        </fieldset>
-      </form>
-    );
+      }),
+      // iban: !newForm.iban
+      //   ? undefined //optional field
+      //   : !IBAN_REGEX.test(newForm.iban)
+      //     ? i18n.str`IBAN should have just uppercased letters and numbers`
+      //     : validateIBAN(newForm.iban, i18n),
+      name: !newForm.name ? i18n.str`required` : undefined,
+      username: !newForm.username ? i18n.str`required` : undefined,
+    });
+    setErrors(errors);
+    setForm(newForm);
+    onChange(errors === undefined ? (newForm as any) : undefined);
   }
-  
-  function initializeFromTemplate(
-    account: SandboxBackend.Circuit.CircuitAccountData | undefined,
-  ): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
-    const emptyAccount = {
-      cashout_address: undefined,
-      iban: undefined,
-      name: undefined,
-      username: undefined,
-      contact_data: undefined,
-    };
-    const emptyContact = {
-      email: undefined,
-      phone: undefined,
-    };
-  
-    const initial: 
PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
-      structuredClone(account) ?? emptyAccount;
-    if (typeof initial.contact_data === "undefined") {
-      initial.contact_data = emptyContact;
-    }
-    initial.contact_data.email;
-    return initial as any;
+
+  return (
+    <form
+      class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl 
md:col-span-2"
+      autoCapitalize="none"
+      autoCorrect="off"
+      onSubmit={e => {
+        e.preventDefault()
+      }}
+    >
+      <div class="px-4 py-6 sm:p-8">
+        <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+
+
+          <div class="sm:col-span-5">
+            <label
+              class="block text-sm font-medium leading-6 text-gray-900"
+              for="username"
+            >
+              {i18n.str`Username`}
+              {purpose === "create" && <b style={{ color: "red" }}> *</b>}
+            </label>
+            <div class="mt-2">
+              <input
+                ref={ref}
+                type="text"
+                class="block w-full disabled:bg-gray-100 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+                name="username"
+                id="username"
+                data-error={!!errors?.username && form.username !== undefined}
+                disabled={purpose !== "create"}
+                value={form.username ?? ""}
+                onChange={(e) => {
+                  form.username = e.currentTarget.value;
+                  updateForm(structuredClone(form));
+                }}
+                // placeholder=""
+                autocomplete="off"
+              />
+              <ShowInputErrorLabel
+                message={errors?.username}
+                isDirty={form.username !== undefined}
+              />
+            </div>
+            <p class="mt-2 text-sm text-gray-500" >
+              <i18n.Translate>account identification in the 
bank</i18n.Translate>
+            </p>
+          </div>
+
+          <div class="sm:col-span-5">
+            <label
+              class="block text-sm font-medium leading-6 text-gray-900"
+              for="name"
+            >
+              {i18n.str`Name`}
+              {purpose === "create" && <b style={{ color: "red" }}> *</b>}
+            </label>
+            <div class="mt-2">
+              <input
+                type="text"
+                class="block w-full disabled:bg-gray-100 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+                name="name"
+                data-error={!!errors?.name && form.name !== undefined}
+                id="name"
+                disabled={purpose !== "create"}
+                value={form.name ?? ""}
+                onChange={(e) => {
+                  form.name = e.currentTarget.value;
+                  updateForm(structuredClone(form));
+                }}
+                // placeholder=""
+                autocomplete="off"
+              />
+              <ShowInputErrorLabel
+                message={errors?.name}
+                isDirty={form.name !== undefined}
+              />
+            </div>
+            <p class="mt-2 text-sm text-gray-500" >
+              <i18n.Translate>name of the person owner the 
account</i18n.Translate>
+            </p>
+          </div>
+
+
+          {purpose !== "create" && (<div class="sm:col-span-5">
+            <label
+              class="block text-sm font-medium leading-6 text-gray-900"
+              for="internal-iban"
+            >
+              {i18n.str`Internal IBAN`}
+            </label>
+            <div class="mt-2">
+              <input
+                type="text"
+                class="block w-full disabled:bg-gray-100 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+                name="internal-iban"
+                id="internal-iban"
+                disabled={true}
+                value={form.iban ?? ""}
+              />
+            </div>
+            <p class="mt-2 text-sm text-gray-500" >
+              <i18n.Translate>international bank account 
number</i18n.Translate>
+            </p>
+          </div>)}
+
+          <div class="sm:col-span-5">
+            <label
+              class="block text-sm font-medium leading-6 text-gray-900"
+              for="email"
+            >
+              {i18n.str`Email`}
+              {purpose === "create" && <b style={{ color: "red" }}> *</b>}
+            </label>
+            <div class="mt-2">
+              <input
+                type="email"
+                class="block w-full disabled:bg-gray-100 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+                name="email"
+                id="email"
+                data-error={!!errors?.contact_data?.email && 
form.contact_data.email !== undefined}
+                disabled={purpose !== "create"}
+                value={form.contact_data.email ?? ""}
+                onChange={(e) => {
+                  form.contact_data.email = e.currentTarget.value;
+                  updateForm(structuredClone(form));
+                }}
+                autocomplete="off"
+              />
+              <ShowInputErrorLabel
+                message={errors?.contact_data?.email}
+                isDirty={form.contact_data.email !== undefined}
+              />
+            </div>
+          </div>
+
+          <div class="sm:col-span-5">
+            <label
+              class="block text-sm font-medium leading-6 text-gray-900"
+              for="phone"
+            >
+              {i18n.str`Phone`}
+              {purpose === "create" && <b style={{ color: "red" }}> *</b>}
+            </label>
+            <div class="mt-2">
+              <input
+                type="text"
+                class="block w-full disabled:bg-gray-100 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+                name="phone"
+                id="phone"
+                disabled={purpose !== "create"}
+                value={form.contact_data.phone ?? ""}
+                data-error={!!errors?.contact_data?.phone && 
form.contact_data.phone !== undefined}
+                onChange={(e) => {
+                  form.contact_data.phone = e.currentTarget.value;
+                  updateForm(structuredClone(form));
+                }}
+                // placeholder=""
+                autocomplete="off"
+              />
+              <ShowInputErrorLabel
+                message={errors?.contact_data?.phone}
+                isDirty={form.contact_data.phone !== undefined}
+              />
+            </div>
+          </div>
+
+
+          <div class="sm:col-span-5">
+            <label
+              class="block text-sm font-medium leading-6 text-gray-900"
+              for="cashout"
+            >
+              {i18n.str`Cashout IBAN`}
+              {purpose !== "show" && <b style={{ color: "red" }}> *</b>}
+            </label>
+            <div class="mt-2">
+              <input
+                type="text"
+                data-error={!!errors?.cashout_address && form.cashout_address 
!== undefined}
+                class="block w-full disabled:bg-gray-100 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+                name="cashout"
+                id="cashout"
+                disabled={purpose === "show"}
+                value={form.cashout_address ?? ""}
+                onChange={(e) => {
+                  form.cashout_address = e.currentTarget.value;
+                  updateForm(structuredClone(form));
+                }}
+                autocomplete="off"
+              />
+              <ShowInputErrorLabel
+                message={errors?.cashout_address}
+                isDirty={form.cashout_address !== undefined}
+              />
+            </div>
+            <p class="mt-2 text-sm text-gray-500" >
+              <i18n.Translate>account number where the money is going to be 
sent when doing cashouts</i18n.Translate>
+            </p>
+          </div>
+
+        </div>
+      </div>
+      {children}
+    </form>
+  );
+}
+
+function initializeFromTemplate(
+  account: SandboxBackend.Circuit.CircuitAccountData | undefined,
+): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
+  const emptyAccount = {
+    cashout_address: undefined,
+    iban: undefined,
+    name: undefined,
+    username: undefined,
+    contact_data: undefined,
+  };
+  const emptyContact = {
+    email: undefined,
+    phone: undefined,
+  };
+
+  const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
+    structuredClone(account) ?? emptyAccount;
+  if (typeof initial.contact_data === "undefined") {
+    initial.contact_data = emptyContact;
   }
-  
-  
-  
\ No newline at end of file
+  initial.contact_data.email;
+  return initial as any;
+}
+
+
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx 
b/packages/demobank-ui/src/pages/admin/AccountList.tsx
index 56b15818b..56d9c45f9 100644
--- a/packages/demobank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -9,10 +9,10 @@ interface Props {
     onAction: (type: AccountAction, account: string) => void;
     account: string | undefined;
     onRegister: () => void;
-
+    onCreateAccount: () => void;
 }
 
-export function AccountList({ account, onAction, onRegister }: Props): VNode {
+export function AccountList({ account, onAction, onCreateAccount, onRegister 
}: Props): VNode {
     const result = useBusinessAccounts({ account });
     const { i18n } = useTranslationContext();
 
@@ -22,48 +22,60 @@ export function AccountList({ account, onAction, onRegister 
}: Props): VNode {
     }
 
     const { customers } = result.data;
-    return <section
-        id="main"
-        style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
-    >
-        {!customers.length ? (
-            <div></div>
-        ) : (
-            <article>
-                <h2>{i18n.str`Accounts:`}</h2>
-                <div class="results">
-                    <table class="pure-table pure-table-striped">
-                        <thead>
-                            <tr>
-                                <th>{i18n.str`Username`}</th>
-                                <th>{i18n.str`Name`}</th>
-                                <th>{i18n.str`Balance`}</th>
-                                <th>{i18n.str`Actions`}</th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            {customers.map((item, idx) => {
-                                const balance = !item.balance
-                                    ? undefined
-                                    : Amounts.parse(item.balance.amount);
-                                const balanceIsDebit =
-                                    item.balance &&
-                                    item.balance.credit_debit_indicator == 
"debit";
-                                return (
-                                    <tr key={idx}>
-                                        <td>
-                                            <a
-                                                href="#"
-                                                onClick={(e) => {
-                                                    e.preventDefault();
-                                                    onAction("show-details", 
item.username)
-                                                }}
-                                            >
-                                                {item.username}
-                                            </a>
+    return <div class="px-4 sm:px-6 lg:px-8">
+        <div class="sm:flex sm:items-center">
+            <div class="sm:flex-auto">
+                <h1 class="text-base font-semibold leading-6 text-gray-900">
+                    <i18n.Translate>Accounts</i18n.Translate>
+                </h1>
+                <p class="mt-2 text-sm text-gray-700">
+                    <i18n.Translate>A list of all business account in the 
bank.</i18n.Translate>
+                </p>
+            </div>
+            <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
+                <button type="button" class="block rounded-md bg-indigo-600 
px-3 py-2 text-center text-sm font-semibold text-white shadow-sm 
hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 
focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+                    onClick={(e) => {
+                        e.preventDefault()
+                        onCreateAccount()
+                    }}>
+                    <i18n.Translate>Create account</i18n.Translate>
+                </button>
+            </div>
+        </div>
+        <div class="mt-8 flow-root">
+            <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
+                <div class="inline-block min-w-full py-2 align-middle sm:px-6 
lg:px-8">
+                    {!customers.length ? (
+                        <div></div>
+                    ) : (
+                        <table class="min-w-full divide-y divide-gray-300">
+                            <thead>
+                                <tr>
+                                    <th scope="col" class="py-3.5 pl-4 pr-3 
text-left text-sm font-semibold text-gray-900 sm:pl-0">{i18n.str`Username`}</th>
+                                    <th scope="col" class="px-3 py-3.5 
text-left text-sm font-semibold text-gray-900">{i18n.str`Name`}</th>
+                                    <th scope="col" class="px-3 py-3.5 
text-left text-sm font-semibold text-gray-900">{i18n.str`Balance`}</th>
+                                    <th scope="col" class="relative py-3.5 
pl-3 pr-4 sm:pr-0">
+                                        <span 
class="sr-only">{i18n.str`Actions`}</span>
+                                    </th>
+                                </tr>
+                            </thead>
+                            <tbody class="divide-y divide-gray-200">
+                                {customers.map((item, idx) => {
+                                    const balance = !item.balance
+                                        ? undefined
+                                        : Amounts.parse(item.balance.amount);
+                                    const balanceIsDebit =
+                                        item.balance &&
+                                        item.balance.credit_debit_indicator == 
"debit";
+
+                                    return <tr key={idx}>
+                                        <td class="whitespace-nowrap py-4 pl-4 
pr-3 text-sm font-medium text-gray-900 sm:pl-0">
+                                            {item.username}
                                         </td>
-                                        <td>{item.name}</td>
-                                        <td>
+                                        <td class="whitespace-nowrap px-3 py-4 
text-sm text-gray-500">
+                                            {item.name}
+                                        </td>
+                                        <td class="whitespace-nowrap px-3 py-4 
text-sm text-gray-500">
                                             {!balance ? (
                                                 i18n.str`unknown`
                                             ) : (
@@ -77,9 +89,8 @@ export function AccountList({ account, onAction, onRegister 
}: Props): VNode {
                                                 </span>
                                             )}
                                         </td>
-                                        <td>
-                                            <a
-                                                href="#"
+                                        <td class="relative whitespace-nowrap 
py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
+                                            <a href="#" class="text-indigo-600 
hover:text-indigo-900"
                                                 onClick={(e) => {
                                                     e.preventDefault();
                                                     
onAction("update-password", item.username)
@@ -87,34 +98,71 @@ export function AccountList({ account, onAction, onRegister 
}: Props): VNode {
                                             >
                                                 change password
                                             </a>
-                                            &nbsp;
-                                            <a
-                                                href="#"
-                                                onClick={(e) => {
-                                                    e.preventDefault();
-                                                    onAction("show-cashout", 
item.username)
-                                                }}
+                                            <br/>
+                                            <a href="#" class="text-indigo-600 
hover:text-indigo-900" onClick={(e) => {
+                                                e.preventDefault();
+                                                onAction("show-cashout", 
item.username)
+                                            }}
                                             >
                                                 cashouts
                                             </a>
-                                            &nbsp;
-                                            <a
-                                                href="#"
-                                                onClick={(e) => {
-                                                    e.preventDefault();
-                                                    onAction("remove-account", 
item.username)
-                                                }}
+                                            <br/>
+                                            <a href="#" class="text-indigo-600 
hover:text-indigo-900" onClick={(e) => {
+                                                e.preventDefault();
+                                                onAction("remove-account", 
item.username)
+                                            }}
                                             >
                                                 remove
                                             </a>
                                         </td>
                                     </tr>
-                                );
-                            })}
-                        </tbody>
-                    </table>
+                                })}
+
+                                {/* <!-- More people... --> */}
+                            </tbody>
+                        </table>
+                    )}
                 </div>
-            </article>
-        )}
-    </section>
+            </div>
+        </div>
+    </div>
+
+    //     return <section
+    //         id="main"
+    //         style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
+    //     >
+    //             <article>
+    //                 <h2>{i18n.str`Accounts:`}</h2>
+    //                 <div class="results">
+    //                     <table class="pure-table pure-table-striped">
+    //                         <tbody>
+    //                                 return (
+    //                                     <tr key={idx}>
+    //                                         <td>
+    //                                             <a
+    //                                                 href="#"
+    //                                                 onClick={(e) => {
+    //                                                     e.preventDefault();
+    //                                                     
onAction("show-details", item.username)
+    //                                                 }}
+    //                                             >
+    // {item.username}
+    //                                             </a>
+    //                                         </td>
+    //                                         <td>{item.name}</td>
+    //                                         <td>
+    //                                             
+    //                                         </td>
+    //                                         <td>
+
+    //                                         </td>
+    //                                     </tr>
+    //                                 );
+    //                             })}
+    //                         </tbody>
+    //                     </table>
+    //                 </div>
+    //             </article>
+    //         )}
+    //     </section>
 }
\ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx 
b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
index 90835d52b..2146fc6f0 100644
--- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -8,100 +8,94 @@ import { getRandomPassword } from "../rnd.js";
 import { AccountForm } from "./AccountForm.js";
 
 export function CreateNewAccount({
-    onClose,
-    onCreateSuccess,
+  onCancel,
+  onCreateSuccess,
 }: {
-    onClose: () => void;
-    onCreateSuccess: (password: string) => void;
+  onCancel: () => void;
+  onCreateSuccess: (password: string) => void;
 }): VNode {
-    const { i18n } = useTranslationContext();
-    const { createAccount } = useAdminAccountAPI();
-    const [submitAccount, setSubmitAccount] = useState<
-        SandboxBackend.Circuit.CircuitAccountData | undefined
-    >();
-    return (
-        <div>
-            <div>
-                <h1 class="nav welcome-text">
-                    <i18n.Translate>New account</i18n.Translate>
-                </h1>
-            </div>
+  const { i18n } = useTranslationContext();
+  const { createAccount } = useAdminAccountAPI();
+  const [submitAccount, setSubmitAccount] = useState<
+    SandboxBackend.Circuit.CircuitAccountData | undefined
+  >();
 
-            <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" 
}}>
-                <AccountForm
-                    template={undefined}
-                    purpose="create"
-                    onChange={(a) => {
-                        setSubmitAccount(a);
-                    }}
-                />
+  async function doCreate() {
+    if (!submitAccount) return;
+    try {
+      const account: SandboxBackend.Circuit.CircuitAccountRequest =
+      {
+        cashout_address: submitAccount.cashout_address,
+        contact_data: submitAccount.contact_data,
+        internal_iban: submitAccount.iban,
+        name: submitAccount.name,
+        username: submitAccount.username,
+        password: getRandomPassword(),
+      };
 
-                <p>
-                    <div style={{ display: "flex", justifyContent: 
"space-between" }}>
-                        <div>
-                            <input
-                                class="pure-button"
-                                type="submit"
-                                value={i18n.str`Close`}
-                                onClick={async (e) => {
-                                    e.preventDefault();
-                                    onClose();
-                                }}
-                            />
-                        </div>
-                        <div>
-                            <input
-                                id="select-exchange"
-                                class="pure-button pure-button-primary content"
-                                disabled={!submitAccount}
-                                type="submit"
-                                value={i18n.str`Confirm`}
-                                onClick={async (e) => {
-                                    e.preventDefault();
+      await createAccount(account);
+      onCreateSuccess(account.password);
+    } catch (error) {
+      if (error instanceof RequestError) {
+        notify(
+          buildRequestErrorMessage(i18n, error.cause, {
+            onClientError: (status) =>
+              status === HttpStatusCode.Forbidden
+                ? i18n.str`The rights to perform the operation are not 
sufficient`
+                : status === HttpStatusCode.BadRequest
+                  ? i18n.str`Server replied that input data was invalid`
+                  : status === HttpStatusCode.Conflict
+                    ? i18n.str`At least one registration detail was not 
available`
+                    : undefined,
+          }),
+        );
+      } else {
+        notifyError(
+          i18n.str`Operation failed, please report`,
+          (error instanceof Error
+            ? error.message
+            : JSON.stringify(error)) as TranslatedString
+        )
+      }
+    }
+  }
 
-                                    if (!submitAccount) return;
-                                    try {
-                                        const account: 
SandboxBackend.Circuit.CircuitAccountRequest =
-                                        {
-                                            cashout_address: 
submitAccount.cashout_address,
-                                            contact_data: 
submitAccount.contact_data,
-                                            internal_iban: submitAccount.iban,
-                                            name: submitAccount.name,
-                                            username: submitAccount.username,
-                                            password: getRandomPassword(),
-                                        };
-
-                                        await createAccount(account);
-                                        onCreateSuccess(account.password);
-                                    } catch (error) {
-                                        if (error instanceof RequestError) {
-                                            notify(
-                                                buildRequestErrorMessage(i18n, 
error.cause, {
-                                                    onClientError: (status) =>
-                                                        status === 
HttpStatusCode.Forbidden
-                                                            ? i18n.str`The 
rights to perform the operation are not sufficient`
-                                                            : status === 
HttpStatusCode.BadRequest
-                                                                ? 
i18n.str`Input data was invalid`
-                                                                : status === 
HttpStatusCode.Conflict
-                                                                    ? 
i18n.str`At least one registration detail was not available`
-                                                                    : 
undefined,
-                                                }),
-                                            );
-                                        } else {
-                                            notifyError(
-                                                i18n.str`Operation failed, 
please report`,
-                                                (error instanceof Error
-                                                    ? error.message
-                                                    : JSON.stringify(error)) 
as TranslatedString
-                                            )
-                                        }
-                                    }
-                                }}
-                            />
-                        </div>
-                    </div>
-                </p>
-            </div>
+  return (
+    <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 
bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+      <div class="px-4 sm:px-0">
+        <h2 class="text-base font-semibold leading-7 text-gray-900">
+          <i18n.Translate>New business account</i18n.Translate>
+        </h2>
+      </div>
+      <AccountForm
+        template={undefined}
+        purpose="create"
+        onChange={(a) => {
+          setSubmitAccount(a);
+        }}
+      >
+        <div class="flex items-center justify-between gap-x-6 border-t 
border-gray-900/10 px-4 py-4 sm:px-8">
+          {onCancel ?
+            <button type="button" class="text-sm font-semibold leading-6 
text-gray-900"
+              onClick={onCancel}
+            >
+              <i18n.Translate>Cancel</i18n.Translate>
+            </button>
+            : <div />
+          }
+          <button type="submit"
+            class="disabled:opacity-50 disabled:cursor-default cursor-pointer 
rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm 
hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 
focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+            disabled={!submitAccount}
+            onClick={(e) => {
+              e.preventDefault()
+              doCreate()
+            }}
+          >
+            <i18n.Translate>Create</i18n.Translate>
+          </button>
         </div>
-    );
+
+      </AccountForm>
+    </div>
+  );
 }
diff --git a/packages/demobank-ui/src/pages/admin/Home.tsx 
b/packages/demobank-ui/src/pages/admin/Home.tsx
index e1ec6cfe0..f7d4e426e 100644
--- a/packages/demobank-ui/src/pages/admin/Home.tsx
+++ b/packages/demobank-ui/src/pages/admin/Home.tsx
@@ -17,17 +17,20 @@ import { RemoveAccount } from "./RemoveAccount.js";
 interface Props {
   onRegister: () => void;
 }
-export type AccountAction = "show-details" | 
-  "show-cashout" | 
-  "update-password" | 
-  "remove-account" | 
+export type AccountAction = "show-details" |
+  "show-cashout" |
+  "update-password" |
+  "remove-account" |
   "show-cashouts-details";
 
 export function AdminHome({ onRegister }: Props): VNode {
   const [action, setAction] = useState<{
     type: AccountAction,
     account: string
-  }>()
+  } | undefined>({
+    type:"remove-account",
+    account:"gnunet-at-sandbox"
+  })
 
   const [createAccount, setCreateAccount] = useState(false);
 
@@ -78,7 +81,7 @@ export function AdminHome({ onRegister }: Props): VNode {
           notifyInfo(i18n.str`Password changed`);
           setAction(undefined);
         }}
-        onClear={() => {
+        onCancel={() => {
           setAction(undefined);
         }}
       />
@@ -89,7 +92,7 @@ export function AdminHome({ onRegister }: Props): VNode {
           notifyInfo(i18n.str`Account removed`);
           setAction(undefined);
         }}
-        onClear={() => {
+        onCancel={() => {
           setAction(undefined);
         }}
       />
@@ -116,7 +119,7 @@ export function AdminHome({ onRegister }: Props): VNode {
   if (createAccount) {
     return (
       <CreateNewAccount
-        onClose={() => setCreateAccount(false)}
+        onCancel={() => setCreateAccount(false)}
         onCreateSuccess={(password) => {
           notifyInfo(
             i18n.str`Account created with password "${password}". The user 
must change the password on the next login.`,
@@ -129,34 +132,18 @@ export function AdminHome({ onRegister }: Props): VNode {
 
   return (
     <Fragment>
-      <div>
-        <h1 class="nav welcome-text">
-          <i18n.Translate>Admin panel</i18n.Translate>
-        </h1>
-      </div>
 
-      <p>
-        <div style={{ display: "flex", justifyContent: "space-between" }}>
-          <div></div>
-          <div>
-            <input
-              class="pure-button pure-button-primary content"
-              type="submit"
-              value={i18n.str`Create account`}
-              onClick={async (e) => {
-                e.preventDefault();
-
-                setCreateAccount(true);
-              }}
-            />
-          </div>
-        </div>
-      </p>
+      <AccountList
+        onCreateAccount={() => {
+          setCreateAccount(true);
+        }}
+        account={undefined}
+        onAction={(type, account) => setAction({ account, type })}
+        onRegister={onRegister}
+      />
 
       <AdminAccount onRegister={onRegister} />
 
-      <AccountList account={undefined} onAction={(type,account) => 
setAction({account, type})} onRegister={onRegister}/>
-
     </Fragment>
   );
 }
\ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx 
b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index 2900db9d2..050f1fb8a 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -1,112 +1,218 @@
 import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, 
useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode,h,Fragment } from "preact";
+import { VNode, h, Fragment } from "preact";
 import { useAccountDetails } from "../../hooks/access.js";
 import { useAdminAccountAPI } from "../../hooks/circuit.js";
 import { Amounts, HttpStatusCode, TranslatedString } from 
"@gnu-taler/taler-util";
-import { buildRequestErrorMessage } from "../../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
 
 export function RemoveAccount({
-    account,
-    onClear,
-    onUpdateSuccess,
-    onLoadNotOk,
-  }: {
-    onLoadNotOk: <T>(
-      error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
-    ) => VNode;
-    onClear: () => void;
-    onUpdateSuccess: () => void;
-    account: string;
-  }): VNode {
-    const { i18n } = useTranslationContext();
-    const result = useAccountDetails(account);
-    const { deleteAccount } = useAdminAccountAPI();
-  
-    if (!result.ok) {
-      if (result.loading || result.type === ErrorType.TIMEOUT) {
-        return onLoadNotOk(result);
-      }
-      if (result.status === HttpStatusCode.NotFound) {
-        return <div>account not found</div>;
-      }
+  account,
+  onCancel,
+  onUpdateSuccess,
+  onLoadNotOk,
+  focus,
+}: {
+  onLoadNotOk: <T>(
+    error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+  ) => VNode;
+  focus?: boolean;
+  onCancel: () => void;
+  onUpdateSuccess: () => void;
+  account: string;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const result = useAccountDetails(account);
+  const [accountName, setAccountName] = useState<string | undefined>()
+  const { deleteAccount } = useAdminAccountAPI();
+
+  if (!result.ok) {
+    if (result.loading || result.type === ErrorType.TIMEOUT) {
       return onLoadNotOk(result);
     }
-  
-    const balance = Amounts.parse(result.data.balance.amount);
-    if (!balance) {
-      return <div>there was an error reading the balance</div>;
+    if (result.status === HttpStatusCode.NotFound) {
+      return <div>account not found</div>;
     }
-    const isBalanceEmpty = Amounts.isZero(balance);
-    return (
-      <div>
-        <div>
-          <h1 class="nav welcome-text">
-            <i18n.Translate>Remove account: {account}</i18n.Translate>
-          </h1>
+    return onLoadNotOk(result);
+  }
+  const ref = useRef<HTMLInputElement>(null);
+  useEffect(() => {
+    if (focus) ref.current?.focus();
+  }, [focus]);
+
+  const balance = Amounts.parse(result.data.balance.amount);
+  if (!balance) {
+    return <div>there was an error reading the balance</div>;
+  }
+  const isBalanceEmpty = Amounts.isZero(balance);
+  if (!isBalanceEmpty) {
+    return <div>
+      <div class="rounded-md bg-yellow-50 p-4">
+        <div class="flex">
+          <div class="flex-shrink-0">
+            <svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" 
fill="currentColor" aria-hidden="true">
+              <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 
3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 
0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 
01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" 
clip-rule="evenodd" />
+            </svg>
+          </div>
+          <div class="ml-3">
+            <h3 class="text-sm font-medium text-yellow-800">
+              <i18n.Translate>Can't delete the account</i18n.Translate>
+            </h3>
+            <div class="mt-2 text-sm text-yellow-700">
+              <p>
+                <i18n.Translate>The account can be delete while still holding 
some balance. First make sure that the owner make a complete 
cashout.</i18n.Translate>
+              </p>
+            </div>
+          </div>
+
         </div>
-        {/* {FXME: SHOW WARNING} */}
-        {/* {!isBalanceEmpty && (
-          <ErrorBannerFloat
-            error={{
-              title: i18n.str`Can't delete the account`,
-              description: i18n.str`Balance is not empty`,
-            }}
-            onClear={() => saveError(undefined)}
-          />
-        )} */}
-  
-        <p>
-          <div style={{ display: "flex", justifyContent: "space-between" }}>
-            <div>
-              <input
-                class="pure-button"
-                type="submit"
-                value={i18n.str`Cancel`}
-                onClick={async (e) => {
-                  e.preventDefault();
-                  onClear();
-                }}
-              />
+      </div>
+      <div class="mt-2 flex justify-end">
+        <button type="button" class="rounded-md ring-1 ring-gray-400 bg-white 
px-3 py-2 text-sm font-semibold shadow-sm hover:bg-gray-100 
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+          onClick={() => {
+            onCancel()
+          }}>
+          <i18n.Translate>Go back</i18n.Translate>
+        </button>
+      </div>
+    </div>
+  }
+
+  async function doRemove() {
+    try {
+      const r = await deleteAccount(account);
+      onUpdateSuccess();
+    } catch (error) {
+      if (error instanceof RequestError) {
+        notify(
+          buildRequestErrorMessage(i18n, error.cause, {
+            onClientError: (status) =>
+              status === HttpStatusCode.Forbidden
+                ? i18n.str`The administrator specified a institutional 
username`
+                : status === HttpStatusCode.NotFound
+                  ? i18n.str`The username was not found`
+                  : status === HttpStatusCode.PreconditionFailed
+                    ? i18n.str`Balance was not zero`
+                    : undefined,
+          }),
+        );
+      } else {
+        notifyError(i18n.str`Operation failed, please report`,
+          (error instanceof Error
+            ? error.message
+            : JSON.stringify(error)) as TranslatedString);
+      }
+    }
+  }
+
+  const errors = undefinedIfEmpty({
+    accountName: !accountName
+      ? i18n.str`required`
+      : account !== accountName
+        ? i18n.str`name doesn't match`
+        : undefined,
+  });
+
+
+  return (
+    <div>
+      <div class="rounded-md bg-yellow-50 p-4">
+        <div class="flex">
+          <div class="flex-shrink-0">
+            <svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" 
fill="currentColor" aria-hidden="true">
+              <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 
3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 
0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 
01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" 
clip-rule="evenodd" />
+            </svg>
+          </div>
+          <div class="ml-3">
+            <h3 class="text-sm font-bold text-yellow-800">
+              <i18n.Translate>You are going to remove the 
account</i18n.Translate>
+            </h3>
+            <div class="mt-2 text-sm text-yellow-700">
+              <p>
+                <i18n.Translate>This step can't be undone.</i18n.Translate>
+              </p>
             </div>
-            <div>
-              <input
-                id="select-exchange"
-                class="pure-button pure-button-primary content"
-                disabled={!isBalanceEmpty}
-                type="submit"
-                value={i18n.str`Confirm`}
-                onClick={async (e) => {
-                  e.preventDefault();
-                  try {
-                    const r = await deleteAccount(account);
-                    onUpdateSuccess();
-                  } catch (error) {
-                    if (error instanceof RequestError) {
-                      notify(
-                        buildRequestErrorMessage(i18n, error.cause, {
-                          onClientError: (status) =>
-                            status === HttpStatusCode.Forbidden
-                              ? i18n.str`The administrator specified a 
institutional username`
-                              : status === HttpStatusCode.NotFound
-                                ? i18n.str`The username was not found`
-                                : status === HttpStatusCode.PreconditionFailed
-                                  ? i18n.str`Balance was not zero`
-                                  : undefined,
-                        }),
-                      );
-                    } else {
-                      notifyError(i18n.str`Operation failed, please report`,
-                        (error instanceof Error
-                            ? error.message
-                            : JSON.stringify(error)) as TranslatedString);
-                    }
-                  }
-                }}
-              />
+          </div>
+
+        </div>
+      </div>
+
+      <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 
bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+        <div class="px-4 sm:px-0">
+          <h2 class="text-base font-semibold leading-7 text-gray-900">
+            <i18n.Translate>Deleting account "{account}"</i18n.Translate>
+          </h2>
+        </div>
+        <form
+          class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl 
md:col-span-2"
+          autoCapitalize="none"
+          autoCorrect="off"
+          onSubmit={e => {
+            e.preventDefault()
+          }}
+        >
+          <div class="px-4 py-6 sm:p-8">
+            <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+
+              <div class="sm:col-span-5">
+                <label
+                  class="block text-sm font-medium leading-6 text-gray-900"
+                  for="password"
+                >
+                  {i18n.str`Verification`}
+                </label>
+                <div class="mt-2">
+                  <input
+                    ref={ref}
+                    type="text"
+                    class="block w-full rounded-md border-0 py-1.5 
text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+                    name="password"
+                    id="password"
+                    data-error={!!errors?.accountName && accountName !== 
undefined}
+                    value={accountName ?? ""}
+                    onChange={(e) => {
+                      setAccountName(e.currentTarget.value)
+                    }}
+                    placeholder={account}
+                    autocomplete="off"
+                  />
+                  <ShowInputErrorLabel
+                    message={errors?.accountName}
+                    isDirty={accountName !== undefined}
+                  />
+                </div>
+                <p class="mt-2 text-sm text-gray-500" >
+                  <i18n.Translate>enter the account name that is going to be 
deleted</i18n.Translate>
+                </p>
+              </div>
+
+
+
             </div>
           </div>
-        </p>
+          <div class="flex items-center justify-between gap-x-6 border-t 
border-gray-900/10 px-4 py-4 sm:px-8">
+            {onCancel ?
+              <button type="button" class="text-sm font-semibold leading-6 
text-gray-900"
+                onClick={onCancel}
+              >
+                <i18n.Translate>Cancel</i18n.Translate>
+              </button>
+              : <div />
+            }
+            <button type="submit"
+              class="disabled:opacity-50 disabled:cursor-default 
cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white 
shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 
focus-visible:outline-offset-2 focus-visible:outline-red-600"
+              disabled={!!errors}
+              onClick={(e) => {
+                e.preventDefault()
+                doRemove()
+              }}
+            >
+              <i18n.Translate>Delete</i18n.Translate>
+            </button>
+          </div>
+        </form>
       </div>
-    );
-  }
-  
\ No newline at end of file
+    </div>
+  );
+}
diff --git a/packages/demobank-ui/src/pages/business/Home.tsx 
b/packages/demobank-ui/src/pages/business/Home.tsx
index 8beea640a..318a4cfda 100644
--- a/packages/demobank-ui/src/pages/business/Home.tsx
+++ b/packages/demobank-ui/src/pages/business/Home.tsx
@@ -109,7 +109,7 @@ export function BusinessAccount({
           notifyInfo(i18n.str`Password changed`);
           setUpdatePassword(false);
         }}
-        onClear={() => {
+        onCancel={() => {
           setUpdatePassword(false);
         }}
       />

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]