diff options
author | Sebastian <sebasjm@gmail.com> | 2023-10-21 20:25:38 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-10-21 20:25:50 -0300 |
commit | 2ac73949e7cb8de44e56f2fecae617efab15671e (patch) | |
tree | 144a97d71bc9fa964675ef0cc764087ceb14e8eb /packages/demobank-ui/src/components | |
parent | 4b98b693d696d90f30f0a6546b0e1f4bc181a5f2 (diff) | |
download | wallet-core-2ac73949e7cb8de44e56f2fecae617efab15671e.tar.xz |
more ui
Diffstat (limited to 'packages/demobank-ui/src/components')
-rw-r--r-- | packages/demobank-ui/src/components/Cashouts/views.tsx | 159 | ||||
-rw-r--r-- | packages/demobank-ui/src/components/CopyButton.tsx | 32 | ||||
-rw-r--r-- | packages/demobank-ui/src/components/LangSelector.tsx | 3 | ||||
-rw-r--r-- | packages/demobank-ui/src/components/Routing.tsx | 208 | ||||
-rw-r--r-- | packages/demobank-ui/src/components/Transactions/views.tsx | 12 | ||||
-rw-r--r-- | packages/demobank-ui/src/components/app.tsx | 3 |
6 files changed, 323 insertions, 94 deletions
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx index 0602f507e..32fe0aa9e 100644 --- a/packages/demobank-ui/src/components/Cashouts/views.tsx +++ b/packages/demobank-ui/src/components/Cashouts/views.tsx @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { State } from "./index.js"; import { format } from "date-fns"; @@ -33,55 +33,118 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function ReadyView({ cashouts, onSelected }: State.Ready): VNode { const { i18n } = useTranslationContext(); - if (!cashouts.length) { - return ( - <div> - <i18n.Translate>No cashout at the moment</i18n.Translate> - </div> - ); - } + if (!cashouts.length) return <div /> + const txByDate = cashouts.reduce((prev, cur) => { + const d = cur.creation_time.t_s === "never" + ? "" + : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy") + if (!prev[d]) { + prev[d] = [] + } + prev[d].push(cur) + return prev + }, {} as Record<string, typeof cashouts>) return ( - <div class="results"> - <table class="pure-table pure-table-striped"> - <thead> - <tr> - <th>{i18n.str`Created`}</th> - <th>{i18n.str`Confirmed`}</th> - <th>{i18n.str`Total debit`}</th> - <th>{i18n.str`Total credit`}</th> - <th>{i18n.str`Status`}</th> - <th>{i18n.str`Subject`}</th> - </tr> - </thead> - <tbody> - {cashouts.map((item, idx) => { - return ( - <tr key={idx}> - <td>{item.creation_time.t_s === "never" ? i18n.str`never` : format(item.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")}</td> - <td> - {item.confirmation_time + <div class="px-4 mt-4"> + <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>Latest cashouts</i18n.Translate></h1> + </div> + </div> + <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white"> + <table class="min-w-full divide-y divide-gray-300"> + <thead> + <tr> + <th scope="col" class=" pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Created`}</th> + <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Confirmed`}</th> + <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Total debit`}</th> + <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Total credit`}</th> + <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Status`}</th> + <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Subject`}</th> + </tr> + </thead> + <tbody> + {Object.entries(txByDate).map(([date, txs], idx) => { + return <Fragment key={idx}> + <tr class="border-t border-gray-200"> + <th colSpan={4} scope="colgroup" class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3"> + {date} + </th> + </tr> + {txs.map(item => { + const creationTime = item.creation_time.t_s === "never" ? "" : format(item.creation_time.t_s * 1000, "HH:mm:ss") + const confirmationTime = item.confirmation_time ? item.confirmation_time.t_s === "never" ? i18n.str`never` : format(item.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss") - : "-"} - </td> - <td><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} /></td> - <td><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} /></td> - <td>{item.status}</td> - <td> - <a - href="#" - onClick={(e) => { - e.preventDefault(); - onSelected(item.id); - }} - > - {item.subject} - </a> - </td> - </tr> - ); - })} - </tbody> - </table> + : "-" + return (<tr key={idx} class="border-b border-gray-200 last:border-none"> + + <td class="relative py-2 pl-2 pr-2 text-sm "> + <div class="font-medium text-gray-900">{creationTime}</div> + {/* <dl class="font-normal sm:hidden"> + <dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt> + <dd class="mt-1 truncate text-gray-700"> + {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? ( + <span data-negative={item.negative ? "true" : "false"} class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> + <RenderAmount value={item.amount} /> + </span> + ) : ( + <span style={{ color: "grey" }}><{i18n.str`invalid value`}></span> + )}</dd> + + <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt> + <dd class="mt-1 truncate text-gray-500 sm:hidden"> + {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart} + </dd> + <dd class="mt-1 text-gray-500 sm:hidden" > + <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100"> + {item.subject} + </pre> + </dd> + </dl> */} + </td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{confirmationTime}</td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} /></td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} /></td> + + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.status}</td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md"> + <a href="#" onClick={(e) => { + e.preventDefault(); + onSelected(item.id); + }}> + {item.subject} + </a> + </td> + </tr>) + })} + </Fragment> + + })} + </tbody> + + </table> + + {/* <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination"> + <div class="flex flex-1 justify-between sm:justify-end"> + <button + class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + disabled={!onPrev} + onClick={onPrev} + > + <i18n.Translate>First page</i18n.Translate> + </button> + <button + class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + disabled={!onNext} + onClick={onNext} + > + <i18n.Translate>Next</i18n.Translate> + </button> + </div> + </nav> */} + </div> </div> ); + // } + } diff --git a/packages/demobank-ui/src/components/CopyButton.tsx b/packages/demobank-ui/src/components/CopyButton.tsx index b36de770e..ca1ceaa8a 100644 --- a/packages/demobank-ui/src/components/CopyButton.tsx +++ b/packages/demobank-ui/src/components/CopyButton.tsx @@ -5,31 +5,21 @@ import { useEffect, useState } from "preact/hooks"; export function CopyIcon(): VNode { return ( - <svg height="16" viewBox="0 0 16 16" width="16" stroke="currentColor" strokeWidth="1.5"> - <path - fill-rule="evenodd" - d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z" - /> - <path - fill-rule="evenodd" - d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z" - /> + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" /> </svg> ) }; export function CopiedIcon(): VNode { return ( - <svg height="16" viewBox="0 0 16 16" width="16" stroke="currentColor" strokeWidth="1.5"> - <path - fill-rule="evenodd" - d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z" - /> + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /> </svg> ) }; -export function CopyButton({ getContent }: { getContent: () => string }): VNode { +export function CopyButton({ class: clazz, getContent }: { class: string, getContent: () => string }): VNode { const [copied, setCopied] = useState(false); function copyText(): void { navigator.clipboard.writeText(getContent() || ""); @@ -45,16 +35,14 @@ export function CopyButton({ getContent }: { getContent: () => string }): VNode if (!copied) { return ( - <button class="text-white" onClick={copyText} style={{ width: 16, height: 16, fontSize: "initial" }}> + <button class={clazz} onClick={copyText} > <CopyIcon /> </button> ); } return ( - <div class="text-white" content="Copied" style={{ display: "inline-block" }}> - <button disabled style={{ width: 16, height: 16, fontSize: "initial" }}> - <CopiedIcon /> - </button> - </div> + <button class={clazz} disabled> + <CopiedIcon /> + </button> ); -}
\ No newline at end of file +} diff --git a/packages/demobank-ui/src/components/LangSelector.tsx b/packages/demobank-ui/src/components/LangSelector.tsx index c1d0f64ef..7cf0300df 100644 --- a/packages/demobank-ui/src/components/LangSelector.tsx +++ b/packages/demobank-ui/src/components/LangSelector.tsx @@ -23,6 +23,7 @@ import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { strings as messages } from "../i18n/strings.js"; +import langIcon from "../assets/lang.svg"; type LangsNames = { [P in keyof typeof messages]: string; @@ -69,7 +70,7 @@ export function LangSelector(): VNode { setHidden((h) => !h); }}> <span class="flex items-center"> - <img src="https://taler.net/images/languageicon.svg" alt="" class="h-5 w-5 flex-shrink-0 rounded-full" /> + <img src={langIcon} alt="" class="h-5 w-5 flex-shrink-0 rounded-full" /> <span class="ml-3 block truncate">{getLangName(lang)}</span> </span> <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx index 04cf96190..1d587fe32 100644 --- a/packages/demobank-ui/src/components/Routing.tsx +++ b/packages/demobank-ui/src/components/Routing.tsx @@ -19,19 +19,26 @@ import { createHashHistory } from "history"; import { Fragment, VNode, h } from "preact"; import { Route, Router, route } from "preact-router"; import { useEffect } from "preact/hooks"; -import { useBackendContext } from "../context/backend.js"; +import { useBackendState } from "../hooks/backend.js"; import { BankFrame } from "../pages/BankFrame.js"; import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js"; import { LoginForm } from "../pages/LoginForm.js"; import { PublicHistoriesPage } from "../pages/PublicHistoriesPage.js"; import { RegistrationPage } from "../pages/RegistrationPage.js"; -import { AdminHome } from "../pages/admin/Home.js"; -import { BusinessAccount } from "../pages/business/Home.js"; +import { AdminHome } from "../pages/admin/AdminHome.js"; +import { CreateCashout } from "../pages/business/CreateCashout.js"; import { bankUiSettings } from "../settings.js"; +import { ShowAccountDetails } from "../pages/ShowAccountDetails.js"; +import { UpdateAccountPassword } from "../pages/UpdateAccountPassword.js"; +import { RemoveAccount } from "../pages/admin/RemoveAccount.js"; +import { CreateNewAccount } from "../pages/admin/CreateNewAccount.js"; +import { CashoutListForAccount } from "../pages/admin/CashoutListForAccount.js"; +import { ShowCashoutDetails } from "../pages/business/ShowCashoutDetails.js"; +import { WireTransfer } from "../pages/admin/Account.js"; export function Routing(): VNode { const history = createHashHistory(); - const backend = useBackendContext(); + const backend = useBackendState(); const { i18n } = useTranslationContext(); if (backend.state.status === "loggedOut") { @@ -90,7 +97,7 @@ export function Routing(): VNode { const { isUserAdministrator, username } = backend.state return ( - <BankFrame account={backend.state.username}> + <BankFrame account={username}> <Router history={history}> <Route path="/operation/:wopid" @@ -107,6 +114,167 @@ export function Routing(): VNode { path="/public-accounts" component={() => <PublicHistoriesPage />} /> + + <Route + path="/new-account" + component={() => <CreateNewAccount + onCancel={() => { + route("/account") + }} + onCreateSuccess={() => { + route("/account") + }} + />} + /> + + <Route + path="/profile/:account/details" + component={({ account }: { account: string }) => ( + <ShowAccountDetails + account={account} + onUpdateSuccess={() => { + route("/account") + }} + onClear={() => { + route("/account") + }} + /> + )} + /> + + <Route + path="/profile/:account/change-password" + component={({ account }: { account: string }) => ( + <UpdateAccountPassword + focus + account={account} + onUpdateSuccess={() => { + route("/account") + }} + onCancel={() => { + route("/account") + }} + /> + )} + /> + <Route + path="/profile/:account/delete" + component={({ account }: { account: string }) => ( + <RemoveAccount + account={account} + onUpdateSuccess={() => { + route("/account") + }} + onCancel={() => { + route("/account") + }} + /> + )} + /> + + <Route + path="/profile/:account/cashouts" + component={({ account }: { account: string }) => ( + <CashoutListForAccount + account={account} + onSelected={(cid) => { + route(`/cashout/${cid}`) + }} + onClose={() => { + route("/account") + }} + /> + )} + /> + + <Route + path="/my-profile" + component={() => ( + <ShowAccountDetails + account={username} + onUpdateSuccess={() => { + route("/account") + }} + onClear={() => { + route("/account") + }} + /> + )} + /> + <Route + path="/my-password" + component={() => ( + <UpdateAccountPassword + focus + account={username} + onUpdateSuccess={() => { + route("/account") + }} + onCancel={() => { + route("/account") + }} + /> + )} + /> + + <Route + path="/my-cashouts" + component={() => ( + <CashoutListForAccount + account={username} + onSelected={(cid) => { + route(`/cashout/${cid}`) + }} + onClose={() => { + route("/account"); + }} + /> + )} + /> + + <Route + path="/new-cashout" + component={() => ( + <CreateCashout + account={username} + onComplete={(cid) => { + route(`/cashout/${cid}`); + }} + onCancel={() => { + route("/account"); + }} + /> + )} + /> + + <Route + path="/cashout/:cid" + component={({ cid }: { cid: string }) => ( + <ShowCashoutDetails + id={cid} + onCancel={() => { + route("/account"); + }} + /> + )} + /> + + + <Route + path="/wire-transfer/:dest" + component={({ dest }: { dest: string }) => ( + <WireTransfer + toAccount={dest} + onCancel={() => { + route("/account") + }} + onSuccess={() => { + route("/account") + }} + /> + )} + /> + <Route path="/account" component={() => { @@ -115,6 +283,22 @@ export function Routing(): VNode { onRegister={() => { route("/register"); }} + onCreateAccount={() => { + route("/new-account") + }} + onShowAccountDetails={(aid) => { + route(`/profile/${aid}/details`) + }} + onRemoveAccount={(aid) => { + route(`/profile/${aid}/delete`) + }} + onShowCashoutForAccount={(aid) => { + route(`/profile/${aid}/cashouts`) + }} + onUpdateAccountPassword={(aid) => { + route(`/profile/${aid}/change-password`) + + }} />; } else { return <HomePage @@ -132,20 +316,6 @@ export function Routing(): VNode { } }} /> - <Route - path="/business" - component={() => ( - <BusinessAccount - account={username} - onClose={() => { - route("/account"); - }} - onRegister={() => { - route("/register"); - }} - /> - )} - /> <Route default component={Redirect} to="/account" /> </Router> </BankFrame> diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index 5cdb47a0c..47daf8963 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -86,11 +86,13 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt> <dd class="mt-1 truncate text-gray-500 sm:hidden"> - {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart} + {item.negative ? i18n.str`to` : i18n.str`from`} <a href={`#/wire-transfer/${item.counterpart}`} class="text-indigo-600 hover:text-indigo-900"> + {item.counterpart} + </a> </dd> <dd class="mt-1 text-gray-500 sm:hidden" > <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100"> - {item.subject} + {item.subject} </pre> </dd> </dl> @@ -102,7 +104,11 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode <span style={{ color: "grey" }}><{i18n.str`invalid value`}></span> )} </td> - <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500"> + <a href={`#/wire-transfer/${item.counterpart}`} class="text-indigo-600 hover:text-indigo-900"> + {item.counterpart} + </a> + </td> <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td> </tr>) })} diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index beb24da57..55e1178fe 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -27,6 +27,7 @@ import { BankCoreApiProvider } from "../context/config.js"; import { strings } from "../i18n/strings.js"; import { bankUiSettings } from "../settings.js"; import { Routing } from "./Routing.js"; +import { BankFrame } from "../pages/BankFrame.js"; const WITH_LOCAL_STORAGE_CACHE = false; const App: FunctionalComponent = () => { @@ -34,7 +35,7 @@ const App: FunctionalComponent = () => { return ( <TranslationProvider source={strings}> <BackendStateProvider> - <BankCoreApiProvider baseUrl={baseUrl}> + <BankCoreApiProvider baseUrl={baseUrl} frameOnError={BankFrame}> <SWRConfig value={{ provider: WITH_LOCAL_STORAGE_CACHE |