diff options
author | Sebastian <sebasjm@gmail.com> | 2024-03-05 10:02:32 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-03-05 10:02:32 -0300 |
commit | a6b6c6abf3f2c5cc9b20a6204078416ea5fba510 (patch) | |
tree | 28eddeffc37c70b1746ba0b3208e6019e6ed5c69 | |
parent | 63aedafd841f3a3d7d3b7974d4e5b8fbd02afd3d (diff) |
fix #8573
-rw-r--r-- | packages/demobank-ui/src/components/Cashouts/views.tsx | 19 | ||||
-rw-r--r-- | packages/demobank-ui/src/components/Transactions/views.tsx | 32 | ||||
-rw-r--r-- | packages/demobank-ui/src/context/config.ts | 1 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/BankFrame.tsx | 6 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/LoginForm.tsx | 9 | ||||
-rw-r--r-- | packages/web-util/src/components/Attention.tsx | 62 | ||||
-rw-r--r-- | packages/web-util/src/components/Button.tsx | 3 | ||||
-rw-r--r-- | packages/web-util/src/components/GlobalNotificationBanner.tsx | 11 | ||||
-rw-r--r-- | packages/web-util/src/hooks/useNotifications.ts | 47 |
9 files changed, 113 insertions, 77 deletions
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx index db1fdbfc5..90ee6bc2f 100644 --- a/packages/demobank-ui/src/components/Cashouts/views.tsx +++ b/packages/demobank-ui/src/components/Cashouts/views.tsx @@ -148,16 +148,15 @@ export function ReadyView({ locale: dateLocale, }); return ( - <tr + <a + name="cashout details" key={idx} - class="border-b border-gray-200 hover:bg-gray-200 last:border-none" + class="table-row border-b border-gray-200 hover:bg-gray-200 last:border-none" + // class="table-row" + href={routeCashoutDetails.url({ + cid: String(item.id), + })} > - <a - name="cashout details" - href={routeCashoutDetails.url({ - cid: String(item.id), - })} - > <td class="relative py-2 pl-2 pr-2 text-sm "> <div class="font-medium text-gray-900"> {creationTime} @@ -201,10 +200,10 @@ export function ReadyView({ </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> - </a> - </tr> + </a> ); })} </Fragment> diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index ba400b37a..cdf134b2f 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -16,11 +16,10 @@ import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useBankCoreApiContext } from "../../context/config.js"; import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; import { State } from "./index.js"; -import { useAccountDetails } from "../../hooks/access.js"; export function ReadyView({ transactions, @@ -31,21 +30,24 @@ export function ReadyView({ const { i18n, dateLocale } = useTranslationContext(); const { config } = useBankCoreApiContext() - if (!transactions.length) return <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>Transactions history</i18n.Translate> - </h1> + if (!transactions.length) { + return <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>Transactions history</i18n.Translate> + </h1> + </div> </div> - </div> - <Attention type="info" title={i18n.str`No moves in your account yet.`}> - <i18n.Translate> - You can start sending a wire transfer or withdrawing to your wallet. - </i18n.Translate> - </Attention> - </div>; + <Attention type="low" title={i18n.str`No transactions yet.`}> + <i18n.Translate> + You can start sending a wire transfer or withdrawing to your wallet. + </i18n.Translate> + </Attention> + </div>; + } + const txByDate = transactions.reduce( (prev, cur) => { const d = diff --git a/packages/demobank-ui/src/context/config.ts b/packages/demobank-ui/src/context/config.ts index 529108275..e968b7ff4 100644 --- a/packages/demobank-ui/src/context/config.ts +++ b/packages/demobank-ui/src/context/config.ts @@ -239,6 +239,7 @@ class CacheAwareTalerCoreBankHttpClient extends TalerCoreBankHttpClient { if (resp.type === "ok") { await revalidateAccountDetails(); await revalidateCashouts(); + await revalidateTransactions(); } return resp; } diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index b914aa360..b6bfe1cfb 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -144,7 +144,11 @@ export function BankFrame({ </Header> </div> - <GlobalNotificationsBanner /> + <div class="fixed z-20 w-full"> + <div class="mx-auto w-4/5"> + <GlobalNotificationsBanner /> + </div> + </div> <main class="-mt-32 flex-1"> {account && routeAccountDetails && ( diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index 09c0a8785..f0ca447e1 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -15,25 +15,22 @@ */ import { - HttpStatusCode, - TranslatedString, - assertUnreachable, + HttpStatusCode } from "@gnu-taler/taler-util"; import { Button, LocalNotificationBanner, ShowInputErrorLabel, - useLocalNotification, useLocalNotificationHandler, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; +import { RouteDefinition } from "../route.js"; import { undefinedIfEmpty } from "../utils.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; -import { EmptyObject, RouteDefinition } from "../route.js"; /** * Collect and submit login data. diff --git a/packages/web-util/src/components/Attention.tsx b/packages/web-util/src/components/Attention.tsx index b85230a1b..50378e85a 100644 --- a/packages/web-util/src/components/Attention.tsx +++ b/packages/web-util/src/components/Attention.tsx @@ -1,36 +1,51 @@ -import { TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; +import { Duration, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; import { ComponentChildren, Fragment, VNode, h } from "preact"; interface Props { - type?: "info" | "success" | "warning" | "danger", + type?: "info" | "success" | "warning" | "danger" | "low", onClose?: () => void, title: TranslatedString, children?: ComponentChildren, + timeout?: Duration, } -export function Attention({ type = "info", title, children, onClose }: Props): VNode { +export function Attention({ type = "info", title, children, onClose, timeout = Duration.getForever() }: Props): VNode { return <div class={`group attention-${type} mt-2 shadow-lg`}> - <div class="rounded-md group-[.attention-info]:bg-blue-50 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow"> + <style>{` + .progress { + animation: notificationTimeoutBar 3s ease-in-out; + animation-fill-mode:both; + } + + @keyframes notificationTimeoutBar { + 0% { width: 0; } + 100% { width: 100%; } + } + `}</style> + + <div data-timed={timeout.d_ms !== "forever"} class="rounded-md data-[timed=true]:rounded-b-none group-[.attention-info]:bg-blue-50 group-[.attention-low]:bg-gray-100 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow"> <div class="flex"> <div > - <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 group-[.attention-info]:text-blue-400 group-[.attention-warning]:text-yellow-400 group-[.attention-danger]:text-red-400 group-[.attention-success]:text-green-400"> - {(() => { - switch (type) { - case "info": - return <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" /> - case "warning": - return <path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> - case "danger": - return <path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> - case "success": - return <path fill-rule="evenodd" d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" /> - default: - assertUnreachable(type) - } - })()} - </svg> + {type === "low" ? undefined : + <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 group-[.attention-info]:text-blue-400 group-[.attention-warning]:text-yellow-400 group-[.attention-danger]:text-red-400 group-[.attention-success]:text-green-400"> + {(() => { + switch (type) { + case "info": + return <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" /> + case "warning": + return <path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> + case "danger": + return <path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" /> + case "success": + return <path fill-rule="evenodd" d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" /> + default: + assertUnreachable(type) + } + })()} + </svg> + } </div> <div class="ml-3 w-full"> - <h3 class="text-sm group-hover:text-white font-bold group-[.attention-info]:text-blue-800 group-[.attention-success]:text-green-800 group-[.attention-warning]:text-yellow-800 group-[.attention-danger]:text-red-800"> + <h3 class="text-sm font-bold group-[.attention-info]:text-blue-800 group-[.attention-success]:text-green-800 group-[.attention-warning]:text-yellow-800 group-[.attention-danger]:text-red-800"> {title} </h3> <div class="mt-2 text-sm group-[.attention-info]:text-blue-700 group-[.attention-warning]:text-yellow-700 group-[.attention-danger]:text-red-700 group-[.attention-success]:text-green-700"> @@ -53,6 +68,11 @@ export function Attention({ type = "info", title, children, onClose }: Props): V } </div> </div> + {timeout.d_ms === "forever" ? undefined : + <div class="meter group-[.attention-info]:bg-blue-50 group-[.attention-low]:bg-gray-100 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 h-1 relative overflow-hidden -mt-1"> + <span class="w-full h-full block"><span class="h-full block progress group-[.attention-info]:bg-blue-600 group-[.attention-low]:bg-gray-600 group-[.attention-warning]:bg-yellow-600 group-[.attention-danger]:bg-red-600 group-[.attention-success]:bg-green-600"></span></span> + </div> + } </div> } diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx index 758efafcf..26b778eec 100644 --- a/packages/web-util/src/components/Button.tsx +++ b/packages/web-util/src/components/Button.tsx @@ -45,8 +45,7 @@ export function Button<T extends OperationResult<A, B>, A, B>({ handler.onClick().then((resp) => { if (resp) { if (resp.type === "ok") { - // @ts-expect-error this is an operationOk - const result: OperationOk<any> = resp.body + const result: OperationOk<any> = resp // @ts-expect-error this is an operationOk const msg = handler.onOperationSuccess(result) if (msg) { diff --git a/packages/web-util/src/components/GlobalNotificationBanner.tsx b/packages/web-util/src/components/GlobalNotificationBanner.tsx index c8049acc3..b0a06f7e1 100644 --- a/packages/web-util/src/components/GlobalNotificationBanner.tsx +++ b/packages/web-util/src/components/GlobalNotificationBanner.tsx @@ -1,28 +1,27 @@ import { Fragment, VNode, h } from "preact" -import { Attention, useNotifications } from "../index.browser.js" +import { Attention, GLOBAL_NOTIFICATION_TIMEOUT, useNotifications } from "../index.browser.js" export function GlobalNotificationsBanner(): VNode { const notifs = useNotifications() if (notifs.length === 0) return <Fragment /> - return <div class="fixed z-20 w-full p-4"> { + return <Fragment> { notifs.map(n => { switch (n.message.type) { case "error": return <Attention type="danger" title={n.message.title} onClose={() => { n.remove() - }}> + }} timeout={GLOBAL_NOTIFICATION_TIMEOUT}> {n.message.description && <div class="mt-2 text-sm text-red-700"> {n.message.description} </div> } - {/* <MaybeShowDebugInfo info={n.message.debug} /> */} </Attention> case "info": return <Attention type="success" title={n.message.title} onClose={() => { n.remove(); - }} /> + }} timeout={GLOBAL_NOTIFICATION_TIMEOUT} /> } })} - </div> + </Fragment> } diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts index 33e0cdf53..9f955f92d 100644 --- a/packages/web-util/src/hooks/useNotifications.ts +++ b/packages/web-util/src/hooks/useNotifications.ts @@ -1,4 +1,4 @@ -import { OperationFail, OperationOk, OperationResult, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util"; +import { Duration, OperationFail, OperationOk, OperationResult, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util"; import { useEffect, useState } from "preact/hooks"; import { ButtonHandler } from "../components/Button.js"; import { InternationalizationAPI, memoryMap, useTranslationContext } from "../index.browser.js"; @@ -19,10 +19,28 @@ export interface InfoNotification { const storage = memoryMap<Map<string, NotificationMessage>>(); const NOTIFICATION_KEY = "notification"; +export const GLOBAL_NOTIFICATION_TIMEOUT: Duration = { d_ms: 3 * 1000 } + +function removeFromStorage(n: NotificationMessage) { + const h = hash(n) + const mem = storage.get(NOTIFICATION_KEY) ?? new Map(); + const newState = new Map(mem); + newState.delete(h); + storage.set(NOTIFICATION_KEY, newState); +} + + export function notify(notif: NotificationMessage): void { const currentState: Map<string, NotificationMessage> = storage.get(NOTIFICATION_KEY) ?? new Map(); const newState = currentState.set(hash(notif), notif); + + if (GLOBAL_NOTIFICATION_TIMEOUT.d_ms !== "forever") { + setTimeout(() => { + removeFromStorage(notif) + }, GLOBAL_NOTIFICATION_TIMEOUT.d_ms); + } + storage.set(NOTIFICATION_KEY, newState); } export function notifyError( @@ -73,10 +91,7 @@ export function useNotifications(): Notification[] { return { message, remove: () => { - const mem = storage.get(NOTIFICATION_KEY) ?? new Map(); - const newState = new Map(mem); - newState.delete(hash(message)); - storage.set(NOTIFICATION_KEY, newState); + removeFromStorage(message) }, }; }); @@ -124,7 +139,7 @@ export type ErrorNotificationHandler = (cb: (notify: typeof errorMap) => Promise * @returns */ export function useLocalNotification(): [Notification | undefined, (n: NotificationMessage) => void, ErrorNotificationHandler] { - const {i18n} = useTranslationContext(); + const { i18n } = useTranslationContext(); const [value, setter] = useState<NotificationMessage>(); const notif = !value ? undefined : { @@ -154,12 +169,12 @@ export function useLocalNotification(): [Notification | undefined, (n: Notificat return [notif, setter, errorHandling] } -type HandlerMaker = <T extends OperationResult<A, B>,A,B>( +type HandlerMaker = <T extends OperationResult<A, B>, A, B>( onClick: () => Promise<T | undefined>, - onOperationSuccess: ((result:T extends OperationOk<any> ? T :never) => void) | ((result:T extends OperationOk<any> ? T :never) => TranslatedString | undefined), + onOperationSuccess: ((result: T extends OperationOk<any> ? T : never) => void) | ((result: T extends OperationOk<any> ? T : never) => TranslatedString | undefined), onOperationFail: (d: T extends OperationFail<any> ? T : never) => TranslatedString, onOperationComplete?: () => void, -) => ButtonHandler<T,A,B>; +) => ButtonHandler<T, A, B>; export function useLocalNotificationHandler(): [Notification | undefined, HandlerMaker, (n: NotificationMessage) => void] { const [value, setter] = useState<NotificationMessage>(); @@ -169,20 +184,20 @@ export function useLocalNotificationHandler(): [Notification | undefined, Handle setter(undefined); }, } - - function makeHandler<T extends OperationResult<A, B>,A,B>( + + function makeHandler<T extends OperationResult<A, B>, A, B>( onClick: () => Promise<T | undefined>, - onOperationSuccess: ((result:T extends OperationOk<any> ? T :never) => void) | ((result:T extends OperationOk<any> ? T :never) => TranslatedString | undefined), + onOperationSuccess: ((result: T extends OperationOk<any> ? T : never) => void) | ((result: T extends OperationOk<any> ? T : never) => TranslatedString | undefined), onOperationFail: (d: T extends OperationFail<any> ? T : never) => TranslatedString, - onOperationComplete?: () => void, - ): ButtonHandler<T,A,B> { + onOperationComplete?: () => void, + ): ButtonHandler<T, A, B> { return { onClick, onNotification: setter, onOperationFail, onOperationSuccess, onOperationComplete } } - + return [notif, makeHandler, setter] } -export function buildRequestErrorMessage( i18n: InternationalizationAPI, cause: TalerError): ErrorNotification { +export function buildRequestErrorMessage(i18n: InternationalizationAPI, cause: TalerError): ErrorNotification { let result: ErrorNotification; switch (cause.errorDetail.code) { case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { |