diff options
author | Sebastian <sebasjm@gmail.com> | 2023-11-05 18:04:22 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-11-05 18:04:22 -0300 |
commit | b58d53dd93bd8e97aecc28fae788c5c7051fd73d (patch) | |
tree | 7a402fafa4ae19a64f10eeb3042147f5f4733081 /packages/web-util/src/components | |
parent | 31cf3187e447e2c4ec8a473362c5bacc07a874f1 (diff) | |
download | wallet-core-b58d53dd93bd8e97aecc28fae788c5c7051fd73d.tar.xz |
sharing components in web-util
Diffstat (limited to 'packages/web-util/src/components')
-rw-r--r-- | packages/web-util/src/components/Attention.tsx | 58 | ||||
-rw-r--r-- | packages/web-util/src/components/CopyButton.tsx | 46 | ||||
-rw-r--r-- | packages/web-util/src/components/ErrorLoading.tsx | 119 | ||||
-rw-r--r-- | packages/web-util/src/components/LangSelector.tsx | 111 | ||||
-rw-r--r-- | packages/web-util/src/components/Loading.tsx | 45 | ||||
-rw-r--r-- | packages/web-util/src/components/ShowInputErrorLabel.tsx | 29 | ||||
-rw-r--r-- | packages/web-util/src/components/ShowLocalNotification.tsx | 43 | ||||
-rw-r--r-- | packages/web-util/src/components/index.ts | 7 |
8 files changed, 458 insertions, 0 deletions
diff --git a/packages/web-util/src/components/Attention.tsx b/packages/web-util/src/components/Attention.tsx new file mode 100644 index 000000000..b85230a1b --- /dev/null +++ b/packages/web-util/src/components/Attention.tsx @@ -0,0 +1,58 @@ +import { TranslatedString, assertUnreachable } from "@gnu-taler/taler-util"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; + +interface Props { + type?: "info" | "success" | "warning" | "danger", + onClose?: () => void, + title: TranslatedString, + children?: ComponentChildren, +} +export function Attention({ type = "info", title, children, onClose }: 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"> + <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> + </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"> + {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"> + {children} + </div> + </div> + {onClose && + <div> + <button type="button" class="font-semibold items-center rounded bg-transparent px-2 py-1 text-xs text-gray-900 hover:bg-gray-50" + onClick={(e) => { + e.preventDefault(); + onClose(); + }} + > + <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /> + </svg> + </button> + </div> + } + </div> + </div> + + </div> +} diff --git a/packages/web-util/src/components/CopyButton.tsx b/packages/web-util/src/components/CopyButton.tsx new file mode 100644 index 000000000..0096da365 --- /dev/null +++ b/packages/web-util/src/components/CopyButton.tsx @@ -0,0 +1,46 @@ +import { h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; + +export function CopyIcon(): VNode { + return ( + <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 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({ class: clazz, getContent }: { class: string, getContent: () => string }): VNode { + const [copied, setCopied] = useState(false); + function copyText(): void { + navigator.clipboard.writeText(getContent() || ""); + setCopied(true); + } + useEffect(() => { + if (copied) { + setTimeout(() => { + setCopied(false); + }, 1000); + } + }, [copied]); + + if (!copied) { + return ( + <button class={clazz} onClick={copyText} > + <CopyIcon /> + </button> + ); + } + return ( + <button class={clazz} disabled> + <CopiedIcon /> + </button> + ); +} diff --git a/packages/web-util/src/components/ErrorLoading.tsx b/packages/web-util/src/components/ErrorLoading.tsx new file mode 100644 index 000000000..02f2a3282 --- /dev/null +++ b/packages/web-util/src/components/ErrorLoading.tsx @@ -0,0 +1,119 @@ +/* +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; +import { Attention } from "./Attention.js"; +import { useTranslationContext } from "../index.browser.js"; + +export function ErrorLoading({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode { + const { i18n } = useTranslationContext() + switch (error.errorDetail.code) { + ////////////////// + // Every error that can be produce in a Http Request + ////////////////// + case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { + if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) { + const { requestMethod, requestUrl, timeoutMs } = error.errorDetail + return <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { + if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { + const { requestMethod, requestUrl, throttleStats } = error.errorDetail + return <Attention type="danger" title={i18n.str`A lot of request were made to the same server and this action was throttled`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { + if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) { + const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail + return <Attention type="danger" title={i18n.str`The response of the request is malformed.`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_NETWORK_ERROR: { + if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) { + const { requestMethod, requestUrl } = error.errorDetail + return <Attention type="danger" title={i18n.str`Could not complete the request due to a network problem.`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { + if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) { + const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail + return <Attention type="danger" title={i18n.str`Unexpected request error`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)} + </pre> + } + </Attention> + } + assertUnreachable(1 as never) + } + ////////////////// + // Every other error + ////////////////// + // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { + // return <Attention type="danger" title={i18n.str``}> + // </Attention> + // } + ////////////////// + // Default message for unhandled case + ////////////////// + default: return <Attention type="danger" title={i18n.str`Unexpected error`}> + {error.message} + {showDetail && + <pre class="whitespace-break-spaces "> + {JSON.stringify(error.errorDetail, undefined, 2)} + </pre> + } + </Attention> + } +} + diff --git a/packages/web-util/src/components/LangSelector.tsx b/packages/web-util/src/components/LangSelector.tsx new file mode 100644 index 000000000..a8d910129 --- /dev/null +++ b/packages/web-util/src/components/LangSelector.tsx @@ -0,0 +1,111 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Fragment, h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +// import { strings as messages } from "../i18n/strings.js"; +import langIcon from "../assets/lang.svg"; +import { useTranslationContext } from "../index.browser.js"; + +type LangsNames = { + [P: string]: string; +}; + +const names: LangsNames = { + es: "Español [es]", + en: "English [en]", + fr: "Français [fr]", + de: "Deutsch [de]", + sv: "Svenska [sv]", + it: "Italiano [it]", +}; + +function getLangName(s: keyof LangsNames | string): string { + if (names[s]) return names[s]; + return String(s); +} + +export function LangSelector({ supportedLangs }: { supportedLangs: string[] }): VNode { + const [updatingLang, setUpdatingLang] = useState(false); + const { lang, changeLanguage } = useTranslationContext(); + const [hidden, setHidden] = useState(true); + + useEffect(() => { + function bodyKeyPress(event: KeyboardEvent) { + if (event.code === "Escape") setHidden(true); + } + function bodyOnClick(event: Event) { + setHidden(true); + } + document.body.addEventListener("click", bodyOnClick); + document.body.addEventListener("keydown", bodyKeyPress as any); + return () => { + document.body.removeEventListener("keydown", bodyKeyPress as any); + document.body.removeEventListener("click", bodyOnClick); + }; + }, []); + return ( + <div> + <div class="relative mt-2"> + <button type="button" class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" + onClick={() => { + setHidden((h) => !h); + }}> + <span class="flex items-center"> + <img alt="language" class="h-5 w-5 flex-shrink-0 rounded-full" src={langIcon} /> + <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"> + <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd" /> + </svg> + </span> + </button> + + {!hidden && + <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" tabIndex={-1} role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3"> + {supportedLangs + .filter((l) => l !== lang) + .map((lang) => ( + <li class="text-gray-900 hover:bg-indigo-600 hover:text-white cursor-pointer relative select-none py-2 pl-3 pr-9" role="option" + onClick={() => { + changeLanguage(lang); + setUpdatingLang(false); + setHidden(true) + }} + > + <span class="font-normal block truncate">{getLangName(lang)}</span> + + <span class="text-indigo-600 absolute inset-y-0 right-0 flex items-center pr-4"> + {/* <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" /> + </svg> */} + </span> + </li> + ))} + + </ul> + } + + </div> + </div> + ); +} diff --git a/packages/web-util/src/components/Loading.tsx b/packages/web-util/src/components/Loading.tsx new file mode 100644 index 000000000..b567e9056 --- /dev/null +++ b/packages/web-util/src/components/Loading.tsx @@ -0,0 +1,45 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { h, VNode } from "preact"; + +export function Loading(): VNode { + return ( + <div + class="columns is-centered is-vcentered" + style={{ + width: "100%", + height: "200px", + display: "flex", + margin: "auto", + justifyContent: "center", + }} + > + <Spinner /> + </div> + ); +} + +export function Spinner(): VNode { + return ( + <div class="lds-ring" style={{margin:"auto"}}> + <div /> + <div /> + <div /> + <div /> + </div> + ); +} diff --git a/packages/web-util/src/components/ShowInputErrorLabel.tsx b/packages/web-util/src/components/ShowInputErrorLabel.tsx new file mode 100644 index 000000000..c5840cad9 --- /dev/null +++ b/packages/web-util/src/components/ShowInputErrorLabel.tsx @@ -0,0 +1,29 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { Fragment, h, VNode } from "preact"; + +export function ShowInputErrorLabel({ + isDirty, + message, +}: { + message: string | undefined; + isDirty: boolean; +}): VNode { + if (message && isDirty) + return <div class="text-base" style={{ color: "red" }}>{message}</div>; + return <div class="text-base" style={{ }}> </div>; +} diff --git a/packages/web-util/src/components/ShowLocalNotification.tsx b/packages/web-util/src/components/ShowLocalNotification.tsx new file mode 100644 index 000000000..cb947e536 --- /dev/null +++ b/packages/web-util/src/components/ShowLocalNotification.tsx @@ -0,0 +1,43 @@ +import { h, Fragment, VNode } from "preact"; +import { Attention } from "./Attention.js"; +import { Notification } from "../index.browser.js"; +// import { useSettings } from "../hooks/settings.js"; + +export function ShowLocalNotification({ notification }: { notification?: Notification }): VNode { + if (!notification) return <Fragment /> + switch (notification.message.type) { + case "error": + return <div class="relative"> + <div class="fixed top-0 left-0 right-0 z-20 w-full p-4"> + <Attention type="danger" title={notification.message.title} onClose={() => { + notification.remove() + }}> + {notification.message.description && + <div class="mt-2 text-sm text-red-700"> + {notification.message.description} + </div> + } + {/* <MaybeShowDebugInfo info={notification.message.debug} /> */} + </Attention> + </div> + </div> + case "info": + return <div class="relative"> + <div class="fixed top-0 left-0 right-0 z-20 w-full p-4"> + <Attention type="success" title={notification.message.title} onClose={() => { + notification.remove(); + }} /></div></div> + } +} + + +// function MaybeShowDebugInfo({ info }: { info: any }): VNode { +// const [settings] = useSettings() +// if (settings.showDebugInfo) { +// return <pre class="whitespace-break-spaces "> +// {info} +// </pre> +// } +// return <Fragment /> +// } + diff --git a/packages/web-util/src/components/index.ts b/packages/web-util/src/components/index.ts index 9441e971d..8344d4a7a 100644 --- a/packages/web-util/src/components/index.ts +++ b/packages/web-util/src/components/index.ts @@ -1 +1,8 @@ export * as utils from "./utils.js"; +export * from "./Attention.js"; +export * from "./CopyButton.js"; +export * from "./ErrorLoading.js"; +export * from "./LangSelector.js"; +export * from "./Loading.js"; +export * from "./ShowInputErrorLabel.js"; +export * from "./ShowLocalNotification.js"; |