summaryrefslogtreecommitdiff
path: root/packages/web-util/src/components/Button.tsx
blob: 26b778eec69cbd314f29ab252172d31f661fc5f3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { OperationFail, OperationOk, OperationResult, TalerError, TranslatedString } from "@gnu-taler/taler-util";
// import { NotificationMessage, notifyInfo } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { HTMLAttributes, useEffect, useState, useTransition } from "preact/compat";
import { NotificationMessage, buildRequestErrorMessage, notifyInfo, useTranslationContext } from "../index.browser.js";
// import { useBankCoreApiContext } from "../context/config.js";

// function errorMap<T extends OperationFail<unknown>>(resp: T, map: (d: T["case"]) => TranslatedString): void {

export interface ButtonHandler<T extends OperationResult<A, B>, A, B> {
  onClick: () => Promise<T | undefined>,
  onNotification: (n: NotificationMessage) => void;
  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;
}

interface Props<T extends OperationResult<A, B>, A, B> extends HTMLAttributes<HTMLButtonElement> {
  handler: ButtonHandler<T, A, B> | undefined,
}

/**
 * This button accept an async function and report a notification
 * on error or success.
 * 
 * When the async function is running the inner text will change into
 * a "loading" animation.
 * 
 * @param param0 
 * @returns 
 */
export function Button<T extends OperationResult<A, B>, A, B>({
  handler,
  children,
  disabled,
  onClick:clickEvent,
  ...rest
}: Props<T, A, B>): VNode {
  const {i18n} = useTranslationContext();
  const [running, setRunning] = useState(false)
  return <button {...rest} disabled={disabled || running} onClick={(e) => {
    e.preventDefault();
    if (!handler) { return; }
    setRunning(true)
    handler.onClick().then((resp) => {
      if (resp) {
        if (resp.type === "ok") {
          const result: OperationOk<any> = resp
          // @ts-expect-error this is an operationOk
          const msg = handler.onOperationSuccess(result)
          if (msg) {
            notifyInfo(msg)
          }
        }
        if (resp.type === "fail") {
          // @ts-expect-error this is an operationFail
          const error: OperationFail<any> = resp;
          // @ts-expect-error this is an operationFail
          const title = handler.onOperationFail(error)
          handler.onNotification({
            title,
            type: "error",
            description: error.detail.hint as TranslatedString,
            debug: error.detail,
          })
        }
      }
      if (handler.onOperationComplete) {
        handler.onOperationComplete()
      }
      setRunning(false)
    }).catch(error => {
      console.error(error)
      
      if (error instanceof TalerError) {
        handler.onNotification(buildRequestErrorMessage(i18n, error))
      } else {
        const description = (error instanceof Error ?
          error.message : String(error)) as TranslatedString
  
        handler.onNotification({
          title: i18n.str`Operation failed`,
          type: "error",
          description,
        })
      }

      if (handler.onOperationComplete) {
        handler.onOperationComplete()
      }
      setRunning(false)
    })
  }} >
    {running ? <Wait /> : children}
  </button>
}

function Wait():VNode {
  return <Fragment>
    <style>{`
      #l1 {          width: 120px;
        height: 20px;
        -webkit-mask: radial-gradient(circle closest-side, currentColor 90%, #0000) left/20% 100%;
        background: linear-gradient(currentColor 0 0) left/0% 100% no-repeat #ddd;
        animation: l17 2s infinite steps(6);
      }
      @keyframes l17 {
          100% {background-size:120% 100%}
`}
    </style>
    <div id="l1" />
  </Fragment>
}