aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/demobank-ui/src')
-rw-r--r--packages/demobank-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/demobank-ui/src/components/Attention.tsx59
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx5
-rw-r--r--packages/demobank-ui/src/components/CopyButton.tsx60
-rw-r--r--packages/demobank-ui/src/components/ErrorLoading.tsx (renamed from packages/demobank-ui/src/scss/_misc.scss)47
-rw-r--r--packages/demobank-ui/src/components/LangSelector.tsx78
-rw-r--r--packages/demobank-ui/src/components/QR.tsx5
-rw-r--r--packages/demobank-ui/src/components/Routing.tsx167
-rw-r--r--packages/demobank-ui/src/components/ShowInputErrorLabel.tsx (renamed from packages/demobank-ui/src/pages/ShowInputErrorLabel.tsx)4
-rw-r--r--packages/demobank-ui/src/components/Transactions/index.ts2
-rw-r--r--packages/demobank-ui/src/components/Transactions/state.ts52
-rw-r--r--packages/demobank-ui/src/components/Transactions/views.tsx139
-rw-r--r--packages/demobank-ui/src/components/app.tsx59
-rw-r--r--packages/demobank-ui/src/context/backend.ts4
-rw-r--r--packages/demobank-ui/src/context/config.ts (renamed from packages/demobank-ui/src/scss/_title-bar.scss)62
-rw-r--r--packages/demobank-ui/src/declaration.d.ts213
-rw-r--r--packages/demobank-ui/src/demobank-ui-settings.js21
-rw-r--r--packages/demobank-ui/src/forms/simplest.ts66
-rw-r--r--packages/demobank-ui/src/hooks/access.ts131
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts113
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts29
-rw-r--r--packages/demobank-ui/src/hooks/config.ts59
-rw-r--r--packages/demobank-ui/src/hooks/notification.ts54
-rw-r--r--packages/demobank-ui/src/hooks/settings.ts22
-rw-r--r--packages/demobank-ui/src/hooks/useCredentialsChecker.ts135
-rw-r--r--packages/demobank-ui/src/index.html49
-rw-r--r--packages/demobank-ui/src/index.tsx2
-rw-r--r--packages/demobank-ui/src/pages/AccountPage.tsx170
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/index.ts92
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/state.ts92
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/stories.tsx (renamed from packages/demobank-ui/src/scss/_footer.scss)22
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/test.ts (renamed from packages/demobank-ui/src/scss/_mixins.scss)24
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx93
-rw-r--r--packages/demobank-ui/src/pages/AdminPage.tsx1064
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx609
-rw-r--r--packages/demobank-ui/src/pages/HomePage.tsx96
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx360
-rw-r--r--packages/demobank-ui/src/pages/OperationState/index.ts122
-rw-r--r--packages/demobank-ui/src/pages/OperationState/state.ts265
-rw-r--r--packages/demobank-ui/src/pages/OperationState/stories.tsx (renamed from packages/demobank-ui/src/scss/_tiles.scss)13
-rw-r--r--packages/demobank-ui/src/pages/OperationState/test.ts (renamed from packages/demobank-ui/src/scss/_modal.scss)25
-rw-r--r--packages/demobank-ui/src/pages/OperationState/views.tsx376
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx134
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx640
-rw-r--r--packages/demobank-ui/src/pages/PublicHistoriesPage.tsx16
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx126
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx440
-rw-r--r--packages/demobank-ui/src/pages/Routing.tsx110
-rw-r--r--packages/demobank-ui/src/pages/ShowAccountDetails.tsx167
-rw-r--r--packages/demobank-ui/src/pages/UpdateAccountPassword.tsx177
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx339
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx389
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx119
-rw-r--r--packages/demobank-ui/src/pages/admin/Account.tsx38
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx315
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx132
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx101
-rw-r--r--packages/demobank-ui/src/pages/admin/Home.tsx148
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx171
-rw-r--r--packages/demobank-ui/src/pages/business/Home.tsx (renamed from packages/demobank-ui/src/pages/BusinessAccount.tsx)226
-rw-r--r--packages/demobank-ui/src/pages/rnd.ts2895
-rw-r--r--packages/demobank-ui/src/scss/DurationPicker.scss70
-rw-r--r--packages/demobank-ui/src/scss/_aside.scss128
-rw-r--r--packages/demobank-ui/src/scss/_card.scss69
-rw-r--r--packages/demobank-ui/src/scss/_custom-calendar.scss263
-rw-r--r--packages/demobank-ui/src/scss/_form.scss71
-rw-r--r--packages/demobank-ui/src/scss/_hero-bar.scss55
-rw-r--r--packages/demobank-ui/src/scss/_loading.scss51
-rw-r--r--packages/demobank-ui/src/scss/_main-section.scss24
-rw-r--r--packages/demobank-ui/src/scss/_nav-bar.scss144
-rw-r--r--packages/demobank-ui/src/scss/_table.scss179
-rw-r--r--packages/demobank-ui/src/scss/_theme-default.scss136
-rw-r--r--packages/demobank-ui/src/scss/bank.scss353
-rw-r--r--packages/demobank-ui/src/scss/colors-bank.scss31
-rw-r--r--packages/demobank-ui/src/scss/demo.scss167
-rw-r--r--packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttfbin43752 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/fonts/nunito.css22
-rw-r--r--packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eotbin844600 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttfbin844380 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woffbin404384 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2bin283040 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css15109
-rw-r--r--packages/demobank-ui/src/scss/libs/_all.scss29
-rw-r--r--packages/demobank-ui/src/scss/main.css3
-rw-r--r--packages/demobank-ui/src/scss/main.scss5
-rw-r--r--packages/demobank-ui/src/scss/pure.scss1397
-rw-r--r--packages/demobank-ui/src/scss/toggle.scss51
-rw-r--r--packages/demobank-ui/src/settings.ts16
-rw-r--r--packages/demobank-ui/src/stories.test.ts3
-rw-r--r--packages/demobank-ui/src/stories.tsx2
-rw-r--r--packages/demobank-ui/src/utils.ts40
91 files changed, 8551 insertions, 21619 deletions
diff --git a/packages/demobank-ui/src/assets/logo-2021.svg b/packages/demobank-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/demobank-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/demobank-ui/src/components/Attention.tsx b/packages/demobank-ui/src/components/Attention.tsx
new file mode 100644
index 000000000..3313e5796
--- /dev/null
+++ b/packages/demobank-ui/src/components/Attention.tsx
@@ -0,0 +1,59 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { assertUnreachable } from "./Routing.js";
+
+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`}>
+ <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/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx
index 4b7649fb6..a32deb266 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -19,6 +19,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State } from "./index.js";
import { format } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
@@ -62,8 +63,8 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
? format(item.confirmation_time, "dd/MM/yyyy HH:mm:ss")
: "-"}
</td>
- <td>{Amounts.stringifyValue(item.amount_debit)}</td>
- <td>{Amounts.stringifyValue(item.amount_credit)}</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
diff --git a/packages/demobank-ui/src/components/CopyButton.tsx b/packages/demobank-ui/src/components/CopyButton.tsx
new file mode 100644
index 000000000..b36de770e
--- /dev/null
+++ b/packages/demobank-ui/src/components/CopyButton.tsx
@@ -0,0 +1,60 @@
+import { h, VNode } from "preact";
+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>
+ )
+};
+
+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>
+ )
+};
+
+export function CopyButton({ getContent }: { 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="text-white" onClick={copyText} style={{ width: 16, height: 16, fontSize: "initial" }}>
+ <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>
+ );
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/scss/_misc.scss b/packages/demobank-ui/src/components/ErrorLoading.tsx
index 65bd28dbd..ee62671ce 100644
--- a/packages/demobank-ui/src/scss/_misc.scss
+++ b/packages/demobank-ui/src/components/ErrorLoading.tsx
@@ -1,6 +1,7 @@
/*
+/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (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
@@ -14,37 +15,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-.is-user-avatar {
- &.has-max-width {
- max-width: $size-base * 7;
- }
-
- &.is-aligned-center {
- margin: 0 auto;
- }
-
- img {
- margin: 0 auto;
- border-radius: $radius-rounded;
- }
-}
-
-.icon.has-update-mark {
- position: relative;
-
- &:after {
- content: "";
- width: $icon-update-mark-size;
- height: $icon-update-mark-size;
- position: absolute;
- top: 1px;
- right: 1px;
- background-color: $icon-update-mark-color;
- border-radius: $radius-rounded;
- }
+import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { Attention } from "./Attention.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+export function ErrorLoading({ error }: { error: HttpError<SandboxBackend.SandboxError> }): VNode {
+ const { i18n } = useTranslationContext()
+ return (<Attention type="danger" title={error.message as TranslatedString}>
+ <p class="text-sm font-medium text-red-800">Got status "{error.info.status}" on {error.info.url}</p>
+ </Attention>
+ );
}
diff --git a/packages/demobank-ui/src/components/LangSelector.tsx b/packages/demobank-ui/src/components/LangSelector.tsx
index ca4411682..c1d0f64ef 100644
--- a/packages/demobank-ui/src/components/LangSelector.tsx
+++ b/packages/demobank-ui/src/components/LangSelector.tsx
@@ -42,11 +42,11 @@ function getLangName(s: keyof LangsNames | string): string {
return String(s);
}
-// FIXME: explain "like py".
-export function LangSelectorLikePy(): VNode {
+export function LangSelector(): 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);
@@ -62,51 +62,49 @@ export function LangSelectorLikePy(): VNode {
};
}, []);
return (
- <Fragment>
- <a
- href="#"
- class="pure-button"
- name="language"
- onClick={(ev) => {
- ev.preventDefault();
- setHidden((h) => !h);
- ev.stopPropagation();
- }}
- >
- {getLangName(lang)}
- </a>
- <div
- id="lang"
- class={hidden ? "hide" : ""}
- style={{
- display: "inline-block",
- }}
- >
- <div style="position: relative; overflow: visible;">
- <div
- class="nav"
- style="position: absolute; max-height: 60vh; overflow-y: auto; margin-left: -120px; margin-top: 20px"
- >
+ <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 src="https://taler.net/images/languageicon.svg" 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">
+ <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">
{Object.keys(messages)
.filter((l) => l !== lang)
- .map((l) => (
- <a
- key={l}
- href="#"
- class="navbtn langbtn"
- value={l}
+ .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(l);
+ changeLanguage(lang);
setUpdatingLang(false);
+ setHidden(true)
}}
>
- {getLangName(l)}
- </a>
+ <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>
))}
- <br />
- </div>
- </div>
+
+ </ul>
+ }
+
</div>
- </Fragment>
+ </div>
);
}
diff --git a/packages/demobank-ui/src/components/QR.tsx b/packages/demobank-ui/src/components/QR.tsx
index c1c159ef8..945a08867 100644
--- a/packages/demobank-ui/src/components/QR.tsx
+++ b/packages/demobank-ui/src/components/QR.tsx
@@ -33,7 +33,6 @@ export function QR({ text }: { text: string }): VNode {
return (
<div
style={{
- width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "left",
@@ -41,9 +40,7 @@ export function QR({ text }: { text: string }): VNode {
>
<div
style={{
- width: "50%",
- minWidth: 200,
- maxWidth: 300,
+ width: "100%",
marginRight: "auto",
marginLeft: "auto",
}}
diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx
new file mode 100644
index 000000000..aafc95687
--- /dev/null
+++ b/packages/demobank-ui/src/components/Routing.tsx
@@ -0,0 +1,167 @@
+/*
+ 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
+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 { 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 { bankUiSettings } from "../settings.js";
+
+export function Routing(): VNode {
+ const history = createHashHistory();
+ const backend = useBackendContext();
+ const {i18n} = useTranslationContext();
+
+ if (backend.state.status === "loggedOut") {
+ return <BankFrame >
+ <Router history={history}>
+ <Route
+ path="/login"
+ component={() => (
+ <Fragment>
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h2>
+ </div>
+
+ <LoginForm
+ onRegister={() => {
+ route("/register");
+ }}
+ />
+ </Fragment>
+ )}
+ />
+ <Route
+ path="/public-accounts"
+ component={() => <PublicHistoriesPage />}
+ />
+ <Route
+ path="/operation/:wopid"
+ component={({ wopid }: { wopid: string }) => (
+ <WithdrawalOperationPage
+ operationId={wopid}
+ onContinue={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ {bankUiSettings.allowRegistrations &&
+ <Route
+ path="/register"
+ component={() => (
+ <RegistrationPage
+ onComplete={() => {
+ route("/account");
+ }}
+ onCancel={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ }
+ <Route default component={Redirect} to="/login" />
+ </Router>
+ </BankFrame>
+ }
+ const { isUserAdministrator, username } = backend.state
+
+ return (
+ <BankFrame account={backend.state.username}>
+ <Router history={history}>
+ <Route
+ path="/operation/:wopid"
+ component={({ wopid }: { wopid: string }) => (
+ <WithdrawalOperationPage
+ operationId={wopid}
+ onContinue={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ <Route
+ path="/public-accounts"
+ component={() => <PublicHistoriesPage />}
+ />
+ <Route
+ path="/account"
+ component={() => {
+ if (isUserAdministrator) {
+ return <AdminHome
+ onRegister={() => {
+ route("/register");
+ }}
+ />;
+ } else {
+ return <HomePage
+ account={username}
+ goToConfirmOperation={(wopid) => {
+ route(`/operation/${wopid}`);
+ }}
+ goToBusinessAccount={() => {
+ route("/business");
+ }}
+ onRegister={() => {
+ route("/register");
+ }}
+ />
+ }
+ }}
+ />
+ <Route
+ path="/business"
+ component={() => (
+ <BusinessAccount
+ account={username}
+ onClose={() => {
+ route("/account");
+ }}
+ onRegister={() => {
+ route("/register");
+ }}
+ onLoadNotOk={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ <Route default component={Redirect} to="/account" />
+ </Router>
+ </BankFrame>
+ );
+}
+
+function Redirect({ to }: { to: string }): VNode {
+ useEffect(() => {
+ route(to, true);
+ }, []);
+ return <div>being redirected to {to}</div>;
+}
+
+export function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
diff --git a/packages/demobank-ui/src/pages/ShowInputErrorLabel.tsx b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
index dacffe20a..c5840cad9 100644
--- a/packages/demobank-ui/src/pages/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/components/Transactions/index.ts b/packages/demobank-ui/src/components/Transactions/index.ts
index 46b38ce74..9df1a70e5 100644
--- a/packages/demobank-ui/src/components/Transactions/index.ts
+++ b/packages/demobank-ui/src/components/Transactions/index.ts
@@ -46,6 +46,8 @@ export namespace State {
status: "ready";
error: undefined;
transactions: Transaction[];
+ onPrev?: () => void;
+ onNext?: () => void;
}
}
diff --git a/packages/demobank-ui/src/components/Transactions/state.ts b/packages/demobank-ui/src/components/Transactions/state.ts
index 09c039055..4b62b005e 100644
--- a/packages/demobank-ui/src/components/Transactions/state.ts
+++ b/packages/demobank-ui/src/components/Transactions/state.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
+import { AbsoluteTime, Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
import { useTransactions } from "../../hooks/access.js";
import { Props, State, Transaction } from "./index.js";
@@ -34,45 +34,19 @@ export function useComponentState({ account }: Props): State {
}
const transactions = result.data.transactions
- .map((item: unknown) => {
- if (
- !item ||
- typeof item !== "object" ||
- !("direction" in item) ||
- !("creditorIban" in item) ||
- !("debtorIban" in item) ||
- !("date" in item) ||
- !("subject" in item) ||
- !("currency" in item) ||
- !("amount" in item)
- ) {
- //not valid
- return;
- }
- const anyItem = item as any;
- if (
- !(typeof anyItem.creditorIban === "string") ||
- !(typeof anyItem.debtorIban === "string") ||
- !(typeof anyItem.date === "string") ||
- !(typeof anyItem.subject === "string") ||
- !(typeof anyItem.currency === "string") ||
- !(typeof anyItem.amount === "string")
- ) {
- return;
- }
+ .map((tx) => {
- const negative = anyItem.direction === "DBIT";
- const counterpart = negative ? anyItem.creditorIban : anyItem.debtorIban;
+ const negative = tx.direction === "debit";
+ const cp = parsePaytoUri(negative ? tx.creditor_payto_uri : tx.debtor_payto_uri);
+ const counterpart = (cp === undefined || !cp.isKnown ? undefined :
+ cp.targetType === "iban" ? cp.iban :
+ cp.targetType === "x-taler-bank" ? cp.account :
+ cp.targetType === "bitcoin" ? `${cp.targetPath.substring(0, 6)}...` : undefined) ??
+ "unkown";
- let date = anyItem.date ? parseInt(anyItem.date, 10) : 0;
- if (isNaN(date) || !isFinite(date)) {
- date = 0;
- }
- const when: AbsoluteTime = !date
- ? AbsoluteTime.never()
- : AbsoluteTime.fromMilliseconds(date);
- const amount = Amounts.parse(`${anyItem.currency}:${anyItem.amount}`);
- const subject = anyItem.subject;
+ const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
+ const amount = Amounts.parse(tx.amount);
+ const subject = tx.subject;
return {
negative,
counterpart,
@@ -87,5 +61,7 @@ export function useComponentState({ account }: Props): State {
status: "ready",
error: undefined,
transactions,
+ onNext: result.isReachingEnd ? undefined : result.loadMore,
+ onPrev: result.isReachingStart ? undefined : result.loadMorePrev,
};
}
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx
index 34d078c16..696fb59f3 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Transactions/views.tsx
@@ -14,11 +14,13 @@
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";
+import { format, isToday } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util";
+import { useEffect, useRef } from "preact/hooks";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
@@ -30,45 +32,104 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
);
}
-export function ReadyView({ transactions }: State.Ready): VNode {
+export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode {
const { i18n } = useTranslationContext();
+ if (!transactions.length) return <div />
+ const txByDate = transactions.reduce((prev, cur) => {
+ const d = cur.when.t_ms === "never"
+ ? ""
+ : format(cur.when.t_ms, "dd/MM/yyyy")
+ if (!prev[d]) {
+ prev[d] = []
+ }
+ prev[d].push(cur)
+ return prev
+ }, {} as Record<string, typeof transactions>)
return (
- <div class="results">
- <table class="pure-table pure-table-striped">
- <thead>
- <tr>
- <th>{i18n.str`Date`}</th>
- <th>{i18n.str`Amount`}</th>
- <th>{i18n.str`Counterpart`}</th>
- <th>{i18n.str`Subject`}</th>
- </tr>
- </thead>
- <tbody>
- {transactions.map((item, idx) => {
- return (
- <tr key={idx}>
- <td>
- {item.when.t_ms === "never"
- ? ""
- : format(item.when.t_ms, "dd/MM/yyyy HH:mm:ss")}
- </td>
- <td>
- {item.negative ? "-" : ""}
- {item.amount ? (
- `${Amounts.stringifyValue(item.amount)} ${
- item.amount.currency
- }`
- ) : (
- <span style={{ color: "grey" }}>&lt;invalid value&gt;</span>
- )}
- </td>
- <td>{item.counterpart}</td>
- <td>{item.subject}</td>
- </tr>
- );
- })}
- </tbody>
- </table>
+ <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 transactions</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`Date`}</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`Amount`}</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`Counterpart`}</th>
+ <th scope="col" class="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>
+ <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 time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss")
+ const amount = <Fragment>
+ { }
+ </Fragment>
+ return (<tr key={idx}>
+ <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <div class="font-medium text-gray-900">{time}</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 ? (
+ <RenderAmount value={item.amount} />
+ ) : (
+ <span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</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>
+ </dl>
+ </td>
+ <td data-negative={item.negative ? "true" : "false"}
+ class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
+ {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} />
+ ) : (
+ <span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
+ )}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td>
+ <td class="px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</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/app.tsx b/packages/demobank-ui/src/components/app.tsx
index ea86da518..7cf658681 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -15,16 +15,23 @@
*/
import {
+ LibtoolVersion,
getGlobalLogLevel,
setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
-import { TranslationProvider } from "@gnu-taler/web-util/browser";
-import { FunctionalComponent, h } from "preact";
+import { TranslationProvider, useApiContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, FunctionalComponent, VNode, h } from "preact";
import { SWRConfig } from "swr";
-import { BackendStateProvider } from "../context/backend.js";
+import { BackendStateProvider, useBackendContext } from "../context/backend.js";
import { strings } from "../i18n/strings.js";
-import { Routing } from "../pages/Routing.js";
-
+import { Routing } from "./Routing.js";
+import { useEffect, useState } from "preact/hooks";
+import { Loading } from "./Loading.js";
+import { getInitialBackendBaseURL } from "../hooks/backend.js";
+import { BANK_INTEGRATION_PROTOCOL_VERSION, useConfigState } from "../hooks/config.js";
+import { ErrorLoading } from "./ErrorLoading.js";
+import { BankFrame } from "../pages/BankFrame.js";
+import { ConfigStateProvider } from "../context/config.js";
const WITH_LOCAL_STORAGE_CACHE = false;
/**
@@ -48,22 +55,44 @@ const App: FunctionalComponent = () => {
return (
<TranslationProvider source={strings}>
<BackendStateProvider>
- <SWRConfig
- value={{
- provider: WITH_LOCAL_STORAGE_CACHE
- ? localStorageProvider
- : undefined,
- }}
- >
- <Routing />
- </SWRConfig>
+ <VersionCheck>
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ }}
+ >
+ <Routing />
+ </SWRConfig>
+ </VersionCheck>
</BackendStateProvider>
- </TranslationProvider>
+ </TranslationProvider >
);
};
(window as any).setGlobalLogLevelFromString = setGlobalLogLevelFromString;
(window as any).getGlobalLevel = getGlobalLogLevel;
+function VersionCheck({ children }: { children: ComponentChildren }): VNode {
+ const checked = useConfigState()
+
+ if (checked === undefined) {
+ return <Loading />
+ }
+ if (checked.type === "wrong") {
+ return <BankFrame>
+ the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}"
+ </BankFrame>
+ }
+ if (checked.type === "ok") {
+ return <ConfigStateProvider value={checked.result}>{children}</ConfigStateProvider>
+ }
+
+ return <BankFrame>
+ <ErrorLoading error={checked.result} />
+ </BankFrame>
+}
+
function localStorageProvider(): Map<unknown, unknown> {
const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
diff --git a/packages/demobank-ui/src/context/backend.ts b/packages/demobank-ui/src/context/backend.ts
index b311ddbb0..eae187c6d 100644
--- a/packages/demobank-ui/src/context/backend.ts
+++ b/packages/demobank-ui/src/context/backend.ts
@@ -34,6 +34,9 @@ const initial: Type = {
logOut() {
null;
},
+ expired() {
+ null;
+ },
logIn(info) {
null;
},
@@ -65,6 +68,7 @@ export const BackendStateProviderTesting = ({
const value: BackendStateHandler = {
state,
logIn: () => {},
+ expired: () => {},
logOut: () => {},
};
diff --git a/packages/demobank-ui/src/scss/_title-bar.scss b/packages/demobank-ui/src/context/config.ts
index 932f8e65d..a2cde18eb 100644
--- a/packages/demobank-ui/src/scss/_title-bar.scss
+++ b/packages/demobank-ui/src/context/config.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (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
@@ -14,37 +14,39 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-section.section.is-title-bar {
- padding: $default-padding;
- border-bottom: $light-border;
-
- ul {
- li {
- display: inline-block;
- padding: 0 $default-padding * 0.5 0 0;
- font-size: $default-padding;
- color: $title-bar-color;
-
- &:after {
- display: inline-block;
- content: "/";
- padding-left: $default-padding * 0.5;
- }
-
- &:last-child {
- padding-right: 0;
- font-weight: 900;
- color: $title-bar-active-color;
-
- &:after {
- display: none;
- }
- }
- }
- }
-}
+export type Type = Required<SandboxBackend.Config>;
+
+const initial: Type = {
+ name: "",
+ version: "0:0:0",
+ currency_fraction_digits: 2,
+ currency_fraction_limit: 2,
+ fiat_currency: "",
+ have_cashout: false,
+};
+const Context = createContext<Type>(initial);
+
+export const useConfigContext = (): Type => useContext(Context);
+
+export const ConfigStateProvider = ({
+ value,
+ children,
+}: {
+ value: Type,
+ children: ComponentChildren;
+}): VNode => {
+
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts
index 462287c59..5c55cfade 100644
--- a/packages/demobank-ui/src/declaration.d.ts
+++ b/packages/demobank-ui/src/declaration.d.ts
@@ -74,7 +74,9 @@ type HashCode = string;
type EddsaPublicKey = string;
type EddsaSignature = string;
type WireTransferIdentifierRawP = string;
-type RelativeTime = Duration;
+type RelativeTime = {
+ d_us: number | "forever"
+};
type ImageDataUrl = string;
interface WithId {
@@ -99,20 +101,33 @@ type Amount = string;
type UUID = string;
type Integer = number;
-interface Balance {
- amount: Amount;
- credit_debit_indicator: "credit" | "debit";
-}
-
namespace SandboxBackend {
export interface Config {
// Name of this API, always "circuit".
name: string;
// API version in the form $n:$n:$n
version: string;
- // Contains ratios and fees related to buying
- // and selling the circuit currency.
- ratios_and_fees: RatiosAndFees;
+ // If 'true', the server provides local currency
+ // conversion support.
+ // If missing or false, some parts of the API
+ // are not supported and return 404.
+ have_cashout?: boolean;
+
+ // Fiat currency. That is the currency in which
+ // cash-out operations ultimately wire money.
+ // Only applicable if have_cashout=true.
+ fiat_currency?: string;
+
+ // How many digits should the amounts be rendered
+ // with by default. Small capitals should
+ // be used to render fractions beyond the number
+ // given here (like on gas stations).
+ currency_fraction_digits?: number;
+
+ // How many decimal digits an operation can
+ // have. Wire transfers with more decimal
+ // digits will not be accepted.
+ currency_fraction_limit?: number;
}
interface RatiosAndFees {
// Exchange rate to buy the circuit currency from fiat.
@@ -126,7 +141,7 @@ namespace SandboxBackend {
}
export interface SandboxError {
- error: SandboxErrorDetail;
+ error?: SandboxErrorDetail;
}
interface SandboxErrorDetail {
// String enum classifying the error.
@@ -152,26 +167,12 @@ namespace SandboxBackend {
UtilError = "util-error",
}
- namespace Access {
- interface PublicAccountsResponse {
- publicAccounts: PublicAccount[];
- }
- interface PublicAccount {
- iban: string;
- balance: string;
- // The account name _and_ the username of the
- // Sandbox customer that owns such a bank account.
- accountLabel: string;
- }
- interface BankAccountBalanceResponse {
- // Available balance on the account.
- balance: Balance;
- // payto://-URI of the account. (New)
- paytoUri: string;
- // Number indicating the max debit allowed for the requesting user.
- debitThreshold: Amount;
- }
+ type EmailAddress = string;
+ type PhoneNumber = string;
+
+ namespace CoreBank {
+
interface BankAccountCreateWithdrawalRequest {
// Amount to withdraw.
amount: Amount;
@@ -213,28 +214,24 @@ namespace SandboxBackend {
}
interface BankAccountTransactionInfo {
- creditorIban: string;
- creditorBic: string; // Optional
- creditorName: string;
+ creditor_payto_uri: string;
+ debtor_payto_uri: string;
- debtorIban: string;
- debtorBic: string;
- debtorName: string;
+ amount: Amount;
+ direction: "debit" | "credit";
- amount: number;
- currency: string;
subject: string;
// Transaction unique ID. Matches
// $transaction_id from the URI.
- uid: string;
- direction: "DBIT" | "CRDT";
- date: string; // milliseconds since the Unix epoch
+ row_id: number;
+ date: Timestamp;
}
+
interface CreateBankAccountTransactionCreate {
// Address in the Payto format of the wire transfer receiver.
// It needs at least the 'message' query string parameter.
- paytoUri: string;
+ payto_uri: string;
// Transaction amount (in the $currency:x.y format), optional.
// However, when not given, its value must occupy the 'amount'
@@ -243,11 +240,143 @@ namespace SandboxBackend {
amount?: string;
}
- interface BankRegistrationRequest {
+ interface RegisterAccountRequest {
+ // Username
username: string;
+ // Password.
password: string;
+
+ // Legal name of the account owner
+ name: string;
+
+ // Defaults to false.
+ is_public?: boolean;
+
+ // Is this a taler exchange account?
+ // If true:
+ // - incoming transactions to the account that do not
+ // have a valid reserve public key are automatically
+ // - the account provides the taler-wire-gateway-api endpoints
+ // Defaults to false.
+ is_taler_exchange?: boolean;
+
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ challenge_contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing a bank account
+ // external to the libeufin-bank.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the local currency
+ // back to fiat currency outside libeufin-bank.
+ cashout_payto_uri?: string;
+
+ // Internal payto URI of this bank account.
+ // Used mostly for testing.
+ internal_payto_uri?: string;
+ }
+ interface ChallengeContactData {
+
+ // E-Mail address
+ email?: EmailAddress;
+
+ // Phone number.
+ phone?: PhoneNumber;
+ }
+
+ interface AccountReconfiguration {
+
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ challenge_contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing a bank account
+ // external to the libeufin-bank.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the local currency
+ // back to fiat currency outside libeufin-bank.
+ cashout_address?: string;
+
+ // Legal name associated with $username.
+ // When missing, the old name is kept.
+ name?: string;
+
+ // If present, change the is_exchange configuration.
+ // See RegisterAccountRequest
+ is_exchange?: boolean;
+ }
+
+
+ interface AccountPasswordChange {
+
+ // New password.
+ new_password: string;
}
+ interface PublicAccountsResponse {
+ public_accounts: PublicAccount[];
+ }
+ interface PublicAccount {
+ payto_uri: string;
+
+ balance: Balance;
+
+ // The account name (=username) of the
+ // libeufin-bank account.
+ account_name: string;
+ }
+
+ interface ListBankAccountsResponse {
+ accounts: AccountMinimalData[];
+ }
+ interface Balance {
+ amount: Amount;
+ credit_debit_indicator: "credit" | "debit";
+ }
+ interface AccountMinimalData {
+ // Username
+ username: string;
+
+ // Legal name of the account owner.
+ name: string;
+
+ // current balance of the account
+ balance: Balance;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: Amount;
+ }
+
+ interface AccountData {
+ // Legal name of the account owner.
+ name: string;
+
+ // Available balance on the account.
+ balance: Balance;
+
+ // payto://-URI of the account.
+ payto_uri: string;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: Amount;
+
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing the bank account
+ // where to send cashouts. This field is optional
+ // because not all the accounts are required to participate
+ // in the merchants' circuit. One example is the exchange:
+ // that never cashouts. Registering these accounts can
+ // be done via the access API.
+ cashout_payto_uri?: string;
+ }
+
}
namespace Circuit {
diff --git a/packages/demobank-ui/src/demobank-ui-settings.js b/packages/demobank-ui/src/demobank-ui-settings.js
new file mode 100644
index 000000000..8a0961831
--- /dev/null
+++ b/packages/demobank-ui/src/demobank-ui-settings.js
@@ -0,0 +1,21 @@
+// Values for development environment
+
+/**
+ * Global settings for the demobank UI.
+ */
+localStorage.setItem("bank-base-url", "http://bank.taler.test/");
+
+globalThis.talerDemobankSettings = {
+ backendBaseURL: "http://bank.taler.test/",
+ allowRegistrations: true,
+ showDemoNav: true,
+ simplePasswordForRandomAccounts: true,
+ allowRandomAccountCreation: true,
+ bankName: "Taler DEVELOPMENT Bank",
+ // Names and links for other demo sites to show in the navbar
+ demoSites: [
+ ["Exchange", "https://Exchnage.taler.test/"],
+ ["Bank", "https://bank-ui.taler.test/"],
+ ["Merchant", "https://merchant.taler.test/"],
+ ],
+};
diff --git a/packages/demobank-ui/src/forms/simplest.ts b/packages/demobank-ui/src/forms/simplest.ts
new file mode 100644
index 000000000..54b6b1c65
--- /dev/null
+++ b/packages/demobank-ui/src/forms/simplest.ts
@@ -0,0 +1,66 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString
+} from "@gnu-taler/taler-util";
+import { DoubleColumnForm, FormState } from "@gnu-taler/web-util/browser";
+
+export namespace Data {
+ export interface WithResolution {
+ when: AbsoluteTime;
+ threshold: AmountJson;
+ state: string;
+ }
+ export interface Form extends WithResolution {
+ comment: string;
+ }
+}
+
+const design: DoubleColumnForm = [
+ {
+ title: "Simple form" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "comment",
+ label: "Comments" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title: "Resolution" as TranslatedString,
+ description: `Current state is and threshold at ` as TranslatedString,
+ fields: [
+ {
+ type: "date",
+ props: {
+ name: "when",
+ label: "Decision Time" as TranslatedString,
+ },
+ },
+ {
+ type: "amount",
+ props: {
+ name: "threshold",
+ label: "New threshold" as TranslatedString,
+ },
+ },
+ ],
+ }
+ ,
+];
+
+function formBehavior(v: Partial<Data.Form>): FormState<Data.Form> {
+ return {
+ when: {
+ disabled: true,
+ },
+ threshold: {
+ // disabled: v.state === AmlExchangeBackend.AmlState.frozen,
+ },
+ };
+}
+
+
diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts
index b8b6ab899..154c43ae6 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -44,13 +44,13 @@ export function useAccessAPI(): AccessAPI {
const account = state.username;
const createWithdrawal = async (
- data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
+ data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest,
): Promise<
- HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>
> => {
const res =
- await request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>(
- `access-api/accounts/${account}/withdrawals`,
+ await request<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>(
+ `accounts/${account}/withdrawals`,
{
method: "POST",
data,
@@ -60,21 +60,21 @@ export function useAccessAPI(): AccessAPI {
return res;
};
const createTransaction = async (
- data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
+ data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(
- `access-api/accounts/${account}/transactions`,
+ `accounts/${account}/transactions`,
{
method: "POST",
data,
contentType: "json",
},
);
- await mutateAll(/.*accounts\/.*\/transactions.*/);
+ await mutateAll(/.*accounts\/.*/);
return res;
};
const deleteAccount = async (): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`access-api/accounts/${account}`, {
+ const res = await request<void>(`accounts/${account}`, {
method: "DELETE",
contentType: "json",
});
@@ -94,7 +94,7 @@ export function useAccessAnonAPI(): AccessAnonAPI {
const { request } = useAuthenticatedBackend();
const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`access-api/withdrawals/${id}/abort`, {
+ const res = await request<void>(`withdrawals/${id}/abort`, {
method: "POST",
contentType: "json",
});
@@ -104,7 +104,7 @@ export function useAccessAnonAPI(): AccessAnonAPI {
const confirmWithdrawal = async (
id: string,
): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`access-api/withdrawals/${id}/confirm`, {
+ const res = await request<void>(`withdrawals/${id}/confirm`, {
method: "POST",
contentType: "json",
});
@@ -122,9 +122,10 @@ export function useTestingAPI(): TestingAPI {
const mutateAll = useMatchMutate();
const { request: noAuthRequest } = usePublicBackend();
const register = async (
- data: SandboxBackend.Access.BankRegistrationRequest,
+ data: SandboxBackend.CoreBank.RegisterAccountRequest,
): Promise<HttpResponseOk<void>> => {
- const res = await noAuthRequest<void>(`access-api/testing/register`, {
+ // FIXME: This API is deprecated. The normal account registration API should be used instead.
+ const res = await noAuthRequest<void>(`accounts`, {
method: "POST",
data,
contentType: "json",
@@ -138,18 +139,18 @@ export function useTestingAPI(): TestingAPI {
export interface TestingAPI {
register: (
- data: SandboxBackend.Access.BankRegistrationRequest,
+ data: SandboxBackend.CoreBank.RegisterAccountRequest,
) => Promise<HttpResponseOk<void>>;
}
export interface AccessAPI {
createWithdrawal: (
- data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
+ data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest,
) => Promise<
- HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>
>;
createTransaction: (
- data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
+ data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
) => Promise<HttpResponseOk<void>>;
deleteAccount: () => Promise<HttpResponseOk<void>>;
}
@@ -166,15 +167,15 @@ export interface InstanceTemplateFilter {
export function useAccountDetails(
account: string,
): HttpResponse<
- SandboxBackend.Access.BankAccountBalanceResponse,
+ SandboxBackend.CoreBank.AccountData,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
- HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>,
+ HttpResponseOk<SandboxBackend.CoreBank.AccountData>,
RequestError<SandboxBackend.SandboxError>
- >([`access-api/accounts/${account}`], fetcher, {
+ >([`accounts/${account}`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -186,28 +187,8 @@ export function useAccountDetails(
keepPreviousData: true,
});
- //FIXME: remove optional when libeufin sandbox has implemented the feature
- if (data && typeof data.data.debitThreshold === "undefined") {
- data.data.debitThreshold = "0";
- }
- //FIXME: sandbox server should return amount string
if (data) {
- const isAmount = Amounts.parse(data.data.debitThreshold);
- if (isAmount) {
- //server response with correct format
- return data;
- }
- const { currency } = Amounts.parseOrThrow(data.data.balance.amount);
- const clone = structuredClone(data);
-
- const theNumber = Number.parseInt(data.data.debitThreshold, 10);
- const value = Number.isNaN(theNumber) ? 0 : theNumber;
- clone.data.debitThreshold = Amounts.stringify({
- currency,
- value: value,
- fraction: 0,
- });
- return clone;
+ return data;
}
if (error) return error.cause;
return { loading: true };
@@ -217,15 +198,15 @@ export function useAccountDetails(
export function useWithdrawalDetails(
wid: string,
): HttpResponse<
- SandboxBackend.Access.BankAccountGetWithdrawalResponse,
+ SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
- HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>,
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse>,
RequestError<SandboxBackend.SandboxError>
- >([`access-api/withdrawals/${wid}`], fetcher, {
+ >([`withdrawals/${wid}`], fetcher, {
refreshInterval: 1000,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -247,15 +228,15 @@ export function useTransactionDetails(
account: string,
tid: string,
): HttpResponse<
- SandboxBackend.Access.BankAccountTransactionInfo,
+ SandboxBackend.CoreBank.BankAccountTransactionInfo,
SandboxBackend.SandboxError
> {
- const { fetcher } = useAuthenticatedBackend();
+ const { paginatedFetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
- HttpResponseOk<SandboxBackend.Access.BankAccountTransactionInfo>,
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionInfo>,
RequestError<SandboxBackend.SandboxError>
- >([`access-api/accounts/${account}/transactions/${tid}`], fetcher, {
+ >([`accounts/${account}/transactions/${tid}`], paginatedFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -274,13 +255,13 @@ export function useTransactionDetails(
}
interface PaginationFilter {
- page: number;
+ // page: number;
}
export function usePublicAccounts(
args?: PaginationFilter,
): HttpResponsePaginated<
- SandboxBackend.Access.PublicAccountsResponse,
+ SandboxBackend.CoreBank.PublicAccountsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = usePublicBackend();
@@ -292,13 +273,13 @@ export function usePublicAccounts(
error: afterError,
isValidating: loadingAfter,
} = useSWR<
- HttpResponseOk<SandboxBackend.Access.PublicAccountsResponse>,
+ HttpResponseOk<SandboxBackend.CoreBank.PublicAccountsResponse>,
RequestError<SandboxBackend.SandboxError>
- >([`access-api/public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher);
+ >([`public-accounts`, page, PAGE_SIZE], paginatedFetcher);
const [lastAfter, setLastAfter] = useState<
HttpResponse<
- SandboxBackend.Access.PublicAccountsResponse,
+ SandboxBackend.CoreBank.PublicAccountsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });
@@ -311,7 +292,7 @@ export function usePublicAccounts(
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
- afterData && afterData.data.publicAccounts.length < PAGE_SIZE;
+ afterData && afterData.data.public_accounts.length < PAGE_SIZE;
const isReachingStart = false;
const pagination = {
@@ -319,7 +300,7 @@ export function usePublicAccounts(
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
- if (afterData.data.publicAccounts.length < MAX_RESULT_SIZE) {
+ if (afterData.data.public_accounts.length < MAX_RESULT_SIZE) {
setPage(page + 1);
}
},
@@ -328,12 +309,12 @@ export function usePublicAccounts(
},
};
- const publicAccounts = !afterData
+ const public_accounts = !afterData
? []
- : (afterData || lastAfter).data.publicAccounts;
- if (loadingAfter) return { loading: true, data: { publicAccounts } };
+ : (afterData || lastAfter).data.public_accounts;
+ if (loadingAfter) return { loading: true, data: { public_accounts } };
if (afterData) {
- return { ok: true, data: { publicAccounts }, ...pagination };
+ return { ok: true, data: { public_accounts }, ...pagination };
}
return { loading: true };
}
@@ -348,28 +329,36 @@ export function useTransactions(
account: string,
args?: PaginationFilter,
): HttpResponsePaginated<
- SandboxBackend.Access.BankAccountTransactionsResponse,
+ SandboxBackend.CoreBank.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = useAuthenticatedBackend();
- const [page, setPage] = useState(1);
+ const [start, setStart] = useState<string>();
const {
data: afterData,
error: afterError,
isValidating: loadingAfter,
} = useSWR<
- HttpResponseOk<SandboxBackend.Access.BankAccountTransactionsResponse>,
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionsResponse>,
RequestError<SandboxBackend.SandboxError>
>(
- [`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE],
- paginatedFetcher,
+ [`accounts/${account}/transactions`, start, PAGE_SIZE],
+ paginatedFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+ // revalidateOnMount: false,
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ }
);
const [lastAfter, setLastAfter] = useState<
HttpResponse<
- SandboxBackend.Access.BankAccountTransactionsResponse,
+ SandboxBackend.CoreBank.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });
@@ -385,19 +374,23 @@ export function useTransactions(
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
afterData && afterData.data.transactions.length < PAGE_SIZE;
- const isReachingStart = false;
+ const isReachingStart = start == undefined;
const pagination = {
isReachingEnd,
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
- if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
- setPage(page + 1);
- }
+ // if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
+ const l = afterData.data.transactions[afterData.data.transactions.length-1]
+ setStart(String(l.row_id));
+ // }
},
loadMorePrev: () => {
- null;
+ if (!afterData || isReachingStart) return;
+ // if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
+ setStart(undefined)
+ // }
},
};
diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts
index 4b60d1b6c..889618646 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -40,21 +40,24 @@ import { useCallback, useEffect, useState } from "preact/hooks";
import { useSWRConfig } from "swr";
import { useBackendContext } from "../context/backend.js";
import { bankUiSettings } from "../settings.js";
+import { AccessToken } from "./useCredentialsChecker.js";
/**
* Has the information to reach and
* authenticate at the bank's backend.
*/
-export type BackendState = LoggedIn | LoggedOut;
+export type BackendState = LoggedIn | LoggedOut | Expired;
-export interface BackendCredentials {
+interface LoggedIn {
+ status: "loggedIn";
+ isUserAdministrator: boolean;
username: string;
- password: string;
+ token: AccessToken;
}
-
-interface LoggedIn extends BackendCredentials {
- status: "loggedIn";
+interface Expired {
+ status: "expired";
isUserAdministrator: boolean;
+ username: string;
}
interface LoggedOut {
status: "loggedOut";
@@ -64,10 +67,17 @@ export const codecForBackendStateLoggedIn = (): Codec<LoggedIn> =>
buildCodecForObject<LoggedIn>()
.property("status", codecForConstString("loggedIn"))
.property("username", codecForString())
- .property("password", codecForString())
+ .property("token", codecForString() as Codec<AccessToken>)
.property("isUserAdministrator", codecForBoolean())
.build("BackendState.LoggedIn");
+export const codecForBackendStateExpired = (): Codec<Expired> =>
+ buildCodecForObject<Expired>()
+ .property("status", codecForConstString("expired"))
+ .property("username", codecForString())
+ .property("isUserAdministrator", codecForBoolean())
+ .build("BackendState.Expired");
+
export const codecForBackendStateLoggedOut = (): Codec<LoggedOut> =>
buildCodecForObject<LoggedOut>()
.property("status", codecForConstString("loggedOut"))
@@ -78,6 +88,7 @@ export const codecForBackendState = (): Codec<BackendState> =>
.discriminateOn("status")
.alternative("loggedIn", codecForBackendStateLoggedIn())
.alternative("loggedOut", codecForBackendStateLoggedOut())
+ .alternative("expired", codecForBackendStateExpired())
.build("BackendState");
export function getInitialBackendBaseURL(): string {
@@ -85,18 +96,27 @@ export function getInitialBackendBaseURL(): string {
typeof localStorage !== "undefined"
? localStorage.getItem("bank-base-url")
: undefined;
+ let result: string;
if (!overrideUrl) {
//normal path
if (!bankUiSettings.backendBaseURL) {
console.error(
"ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
);
- return canonicalizeBaseUrl(window.origin);
+ result = window.origin
+ } else {
+ result = bankUiSettings.backendBaseURL;
}
- return canonicalizeBaseUrl(bankUiSettings.backendBaseURL);
+ } else {
+ // testing/development path
+ result = overrideUrl
+ }
+ try {
+ return canonicalizeBaseUrl(result)
+ } catch (e) {
+ //fall back
+ return canonicalizeBaseUrl(window.origin)
}
- // testing/development path
- return canonicalizeBaseUrl(overrideUrl);
}
export const defaultState: BackendState = {
@@ -106,7 +126,8 @@ export const defaultState: BackendState = {
export interface BackendStateHandler {
state: BackendState;
logOut(): void;
- logIn(info: BackendCredentials): void;
+ expired(): void;
+ logIn(info: {username: string, token: AccessToken}): void;
}
const BACKEND_STATE_KEY = buildStorageKey(
@@ -124,12 +145,22 @@ export function useBackendState(): BackendStateHandler {
BACKEND_STATE_KEY,
defaultState,
);
+ const mutateAll = useMatchMutate();
return {
state,
logOut() {
update(defaultState);
},
+ expired() {
+ if (state.status === "loggedOut") return;
+ const nextState: BackendState = {
+ status: "expired",
+ username: state.username,
+ isUserAdministrator: state.username === "admin",
+ };
+ update(nextState);
+ },
logIn(info) {
//admin is defined by the username
const nextState: BackendState = {
@@ -138,6 +169,7 @@ export function useBackendState(): BackendStateHandler {
isUserAdministrator: info.username === "admin",
};
update(nextState);
+ mutateAll(/.*/)
},
};
}
@@ -150,7 +182,7 @@ interface useBackendType {
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
multiFetcher: <T>(endpoint: string[][]) => Promise<HttpResponseOk<T>[]>;
paginatedFetcher: <T>(
- args: [string, number, number],
+ args: [string, string | undefined, number],
) => Promise<HttpResponseOk<T>>;
sandboxAccountsFetcher: <T>(
args: [string, number, number, string],
@@ -179,13 +211,15 @@ export function usePublicBackend(): useBackendType {
[baseUrl],
);
const paginatedFetcher = useCallback(
- function fetcherImpl<T>([endpoint, page, size]: [
+ function fetcherImpl<T>([endpoint, start, size]: [
string,
- number,
+ string | undefined,
number,
]): Promise<HttpResponseOk<T>> {
+ const delta = -1 * size //descending order
+ const params = start ? { delta, start } : { delta }
return requestHandler<T>(baseUrl, endpoint, {
- params: { page: page || 1, size },
+ params,
});
},
[baseUrl],
@@ -247,35 +281,12 @@ interface InvalidationResult {
error: unknown;
}
-export function useCredentialsChecker() {
- const { request } = useApiContext();
- const baseUrl = getInitialBackendBaseURL();
- //check against account details endpoint
- //while sandbox backend doesn't have a login endpoint
- return async function testLogin(
- username: string,
- password: string,
- ): Promise<CheckResult> {
- try {
- await request(baseUrl, `access-api/accounts/${username}/`, {
- basicAuth: { username, password },
- preventCache: true,
- });
- return { valid: true };
- } catch (error) {
- if (error instanceof RequestError) {
- return { valid: false, requestError: true, cause: error.cause };
- }
- return { valid: false, requestError: false, error };
- }
- };
-}
-
export function useAuthenticatedBackend(): useBackendType {
const { state } = useBackendContext();
const { request: requestHandler } = useApiContext();
- const creds = state.status === "loggedIn" ? state : undefined;
+ // FIXME: libeufin returns 400 insteand of 401 if there is no auth token
+ const creds = state.status === "loggedIn" ? state.token : "secret-token:a";
const baseUrl = getInitialBackendBaseURL();
const request = useCallback(
@@ -283,26 +294,28 @@ export function useAuthenticatedBackend(): useBackendType {
path: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, path, { basicAuth: creds, ...options });
+ return requestHandler<T>(baseUrl, path, { token: creds, ...options });
},
[baseUrl, creds],
);
const fetcher = useCallback(
function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds });
+ return requestHandler<T>(baseUrl, endpoint, { token: creds });
},
[baseUrl, creds],
);
const paginatedFetcher = useCallback(
- function fetcherImpl<T>([endpoint, page = 1, size]: [
+ function fetcherImpl<T>([endpoint, start, size]: [
string,
- number,
+ string | undefined,
number,
]): Promise<HttpResponseOk<T>> {
+ const delta = -1 * size //descending order
+ const params = start ? { delta, start } : { delta }
return requestHandler<T>(baseUrl, endpoint, {
- basicAuth: creds,
- params: { page, size },
+ token: creds,
+ params,
});
},
[baseUrl, creds],
@@ -313,7 +326,7 @@ export function useAuthenticatedBackend(): useBackendType {
> {
return Promise.all(
endpoints.map((endpoint) =>
- requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }),
+ requestHandler<T>(baseUrl, endpoint, { token: creds }),
),
);
},
@@ -327,7 +340,7 @@ export function useAuthenticatedBackend(): useBackendType {
string,
]): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
- basicAuth: creds,
+ token: creds,
params: { page: page || 1, size },
});
},
@@ -339,7 +352,7 @@ export function useAuthenticatedBackend(): useBackendType {
HttpResponseOk<T>
> {
return requestHandler<T>(baseUrl, endpoint, {
- basicAuth: creds,
+ token: creds,
params: { account },
});
},
diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts
index 06557b77f..5dba60951 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -33,6 +33,7 @@ import {
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import _useSWR, { SWRHook } from "swr";
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
+import { AccessToken } from "./useCredentialsChecker.js";
const useSWR = _useSWR as unknown as SWRHook;
export function useAdminAccountAPI(): AdminAccountAPI {
@@ -90,7 +91,8 @@ export function useAdminAccountAPI(): AdminAccountAPI {
await mutateAll(/.*/);
logIn({
username: account,
- password: data.new_password,
+ //FIXME: change password api
+ token: data.new_password as AccessToken,
});
}
return res;
@@ -215,14 +217,15 @@ export interface CircuitAccountAPI {
async function getBusinessStatus(
request: ReturnType<typeof useApiContext>["request"],
- basicAuth: { username: string; password: string },
+ username: string,
+ token: AccessToken,
): Promise<boolean> {
try {
const url = getInitialBackendBaseURL();
const result = await request<SandboxBackend.Circuit.CircuitAccountData>(
url,
- `circuit-api/accounts/${basicAuth.username}`,
- { basicAuth },
+ `circuit-api/accounts/${username}`,
+ { token },
);
return result.ok;
} catch (error) {
@@ -264,10 +267,10 @@ type CashoutEstimators = {
export function useEstimator(): CashoutEstimators {
const { state } = useBackendContext();
const { request } = useApiContext();
- const basicAuth =
- state.status === "loggedOut"
+ const creds =
+ state.status !== "loggedIn"
? undefined
- : { username: state.username, password: state.password };
+ : state.token;
return {
estimateByCredit: async (amount, fee, rate) => {
const zeroBalance = Amounts.zeroOfCurrency(fee.currency);
@@ -282,7 +285,7 @@ export function useEstimator(): CashoutEstimators {
url,
`circuit-api/cashouts/estimates`,
{
- basicAuth,
+ token: creds,
params: {
amount_credit: Amounts.stringify(amount),
},
@@ -313,7 +316,7 @@ export function useEstimator(): CashoutEstimators {
url,
`circuit-api/cashouts/estimates`,
{
- basicAuth,
+ token: creds,
params: {
amount_debit: Amounts.stringify(amount),
},
@@ -337,13 +340,13 @@ export function useBusinessAccountFlag(): boolean | undefined {
const { state } = useBackendContext();
const { request } = useApiContext();
const creds =
- state.status === "loggedOut"
+ state.status !== "loggedIn"
? undefined
- : { username: state.username, password: state.password };
+ : {user: state.username, token: state.token};
useEffect(() => {
if (!creds) return;
- getBusinessStatus(request, creds)
+ getBusinessStatus(request, creds.user, creds.token)
.then((result) => {
setIsBusiness(result);
})
@@ -432,7 +435,7 @@ export function useBusinessAccounts(
HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>,
RequestError<SandboxBackend.SandboxError>
>(
- [`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account],
+ [`accounts`, args?.page, PAGE_SIZE, args?.account],
sandboxAccountsFetcher,
{
refreshInterval: 0,
diff --git a/packages/demobank-ui/src/hooks/config.ts b/packages/demobank-ui/src/hooks/config.ts
new file mode 100644
index 000000000..a3bd294db
--- /dev/null
+++ b/packages/demobank-ui/src/hooks/config.ts
@@ -0,0 +1,59 @@
+import { LibtoolVersion } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, HttpResponseServerError, RequestError, useApiContext } from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { getInitialBackendBaseURL } from "./backend.js";
+
+/**
+ * Protocol version spoken with the bank.
+ *
+ * Uses libtool's current:revision:age versioning.
+ */
+export const BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0";
+
+async function getConfigState(
+ request: ReturnType<typeof useApiContext>["request"],
+): Promise<SandboxBackend.Config> {
+ const url = getInitialBackendBaseURL();
+ const result = await request<SandboxBackend.Config>(url, `config`);
+ return result.data;
+}
+
+export type ConfigResult = undefined
+ | { type: "ok", result: Required<SandboxBackend.Config> }
+ | { type: "wrong", result: SandboxBackend.Config }
+ | { type: "error", result: HttpError<SandboxBackend.SandboxError> }
+
+export function useConfigState(): ConfigResult {
+ const [checked, setChecked] = useState<ConfigResult>()
+ const { request } = useApiContext();
+
+ useEffect(() => {
+ getConfigState(request)
+ .then((result) => {
+ const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version)
+ if (r?.compatible) {
+ const complete: Required<SandboxBackend.Config> = {
+ currency_fraction_digits: result.currency_fraction_digits ?? 2,
+ currency_fraction_limit: result.currency_fraction_limit ?? 2,
+ fiat_currency: "",
+ have_cashout: result.have_cashout ?? false,
+ name: result.name,
+ version: result.version,
+ }
+ setChecked({ type: "ok", result: complete });
+ } else {
+ setChecked({ type: "wrong", result })
+ }
+ })
+ .catch((error: unknown) => {
+ if (error instanceof RequestError) {
+ const result = error.cause
+ setChecked({ type: "error", result });
+ }
+ });
+ }, []);
+
+ return checked;
+}
+
+
diff --git a/packages/demobank-ui/src/hooks/notification.ts b/packages/demobank-ui/src/hooks/notification.ts
deleted file mode 100644
index 9bf621b41..000000000
--- a/packages/demobank-ui/src/hooks/notification.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { memoryMap } from "@gnu-taler/web-util/browser";
-import { StateUpdater, useEffect, useState } from "preact/hooks";
-
-export type NotificationMessage = ErrorNotification | InfoNotification;
-
-//FIXME: this should not be exported since every notification
-// goes throw notify function
-export interface ErrorMessage {
- description?: string;
- title: TranslatedString;
- debug?: string;
-}
-
-interface ErrorNotification {
- type: "error";
- error: ErrorMessage;
-}
-interface InfoNotification {
- type: "info";
- info: TranslatedString;
-}
-
-const storage = memoryMap<NotificationMessage>();
-const NOTIFICATION_KEY = "notification";
-
-export function onNotificationUpdate(
- handler: (newValue: NotificationMessage | undefined) => void,
-) {
- return storage.onUpdate(NOTIFICATION_KEY, () => {
- const newValue = storage.get(NOTIFICATION_KEY);
- handler(newValue);
- });
-}
-
-export function notifyError(error: ErrorMessage) {
- storage.set(NOTIFICATION_KEY, { type: "error", error });
-}
-export function notifyInfo(info: TranslatedString) {
- storage.set(NOTIFICATION_KEY, { type: "info", info });
-}
-
-export function useNotifications(): [
- NotificationMessage | undefined,
- StateUpdater<NotificationMessage | undefined>,
-] {
- const [value, setter] = useState<NotificationMessage | undefined>();
- useEffect(() => {
- return storage.onUpdate(NOTIFICATION_KEY, () => {
- setter(storage.get(NOTIFICATION_KEY));
- });
- });
- return [value, setter];
-}
diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts
index 46b31bf2a..ad853f9d7 100644
--- a/packages/demobank-ui/src/hooks/settings.ts
+++ b/packages/demobank-ui/src/hooks/settings.ts
@@ -15,8 +15,12 @@
*/
import {
+ AmountString,
Codec,
buildCodecForObject,
+ codecForAmountString,
+ codecForBoolean,
+ codecForNumber,
codecForString,
codecOptional,
} from "@gnu-taler/taler-util";
@@ -24,15 +28,33 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
interface Settings {
currentWithdrawalOperationId: string | undefined;
+ showWithdrawalSuccess: boolean;
+ showDemoDescription: boolean;
+ showInstallWallet: boolean;
+ maxWithdrawalAmount: number;
+ fastWithdrawal: boolean;
+ showDebugInfo: boolean;
}
export const codecForSettings = (): Codec<Settings> =>
buildCodecForObject<Settings>()
.property("currentWithdrawalOperationId", codecOptional(codecForString()))
+ .property("showWithdrawalSuccess", (codecForBoolean()))
+ .property("showDemoDescription", (codecForBoolean()))
+ .property("showInstallWallet", (codecForBoolean()))
+ .property("fastWithdrawal", (codecForBoolean()))
+ .property("showDebugInfo", (codecForBoolean()))
+ .property("maxWithdrawalAmount", codecForNumber())
.build("Settings");
const defaultSettings: Settings = {
currentWithdrawalOperationId: undefined,
+ showWithdrawalSuccess: true,
+ showDemoDescription: true,
+ showInstallWallet: true,
+ maxWithdrawalAmount: 25,
+ fastWithdrawal: false,
+ showDebugInfo: false,
};
const DEMOBANK_SETTINGS_KEY = buildStorageKey(
diff --git a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts
new file mode 100644
index 000000000..b3dedb654
--- /dev/null
+++ b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts
@@ -0,0 +1,135 @@
+import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, RequestError, useApiContext } from "@gnu-taler/web-util/browser";
+import { getInitialBackendBaseURL } from "./backend.js";
+
+export function useCredentialsChecker() {
+ const { request } = useApiContext();
+ const baseUrl = getInitialBackendBaseURL();
+ //check against instance details endpoint
+ //while merchant backend doesn't have a login endpoint
+ async function requestNewLoginToken(
+ username: string,
+ password: string,
+ ): Promise<LoginResult> {
+ const data: LoginTokenRequest = {
+ scope: "readwrite" as "write", //FIX: different than merchant
+ duration: {
+ // d_us: "forever" //FIX: should return shortest
+ d_us: 60 * 60 * 24 * 7 * 1000 * 1000
+ },
+ refreshable: true,
+ }
+ try {
+ const response = await request<LoginTokenSuccessResponse>(baseUrl, `accounts/${username}/token`, {
+ method: "POST",
+ basicAuth: {
+ username,
+ password,
+ },
+ data,
+ contentType: "json"
+ });
+ return { valid: true, token: `secret-token:${response.data.access_token}` as AccessToken, expiration: response.data.expiration };
+ } catch (error) {
+ if (error instanceof RequestError) {
+ return { valid: false, cause: error.cause };
+ }
+
+ return {
+ valid: false, cause: {
+ type: ErrorType.UNEXPECTED,
+ loading: false,
+ info: {
+ hasToken: true,
+ status: 0,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ exception: error,
+ message: (error instanceof Error ? error.message : "unpexepected error")
+ }
+ };
+ }
+ };
+
+ async function refreshLoginToken(
+ baseUrl: string,
+ token: LoginToken
+ ): Promise<LoginResult> {
+
+ if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) {
+ return {
+ valid: false, cause: {
+ type: ErrorType.CLIENT,
+ status: HttpStatusCode.Unauthorized,
+ message: "login token expired, login again.",
+ info: {
+ hasToken: true,
+ status: 401,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ payload: {}
+ },
+ }
+ }
+
+ return requestNewLoginToken(baseUrl, token.token)
+ }
+ return { requestNewLoginToken, refreshLoginToken }
+}
+
+export interface LoginToken {
+ token: AccessToken,
+ expiration: Timestamp,
+}
+// token used to get loginToken
+// must forget after used
+declare const __ac_token: unique symbol;
+export type AccessToken = string & {
+ [__ac_token]: true;
+};
+
+type YesOrNo = "yes" | "no";
+export type LoginResult = {
+ valid: true;
+ token: AccessToken;
+ expiration: Timestamp;
+} | {
+ valid: false;
+ cause: HttpError<{}>;
+}
+
+
+// DELETE /private/instances/$INSTANCE
+export interface LoginTokenRequest {
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ duration?: RelativeTime;
+
+ // Can this token be refreshed?
+ // Defaults to false.
+ refreshable?: boolean;
+}
+export interface LoginTokenSuccessResponse {
+ // The login token that can be used to access resources
+ // that are in scope for some time. Must be prefixed
+ // with "Bearer " when used in the "Authorization" HTTP header.
+ // Will already begin with the RFC 8959 prefix.
+ access_token: AccessToken;
+
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ expiration: Timestamp;
+
+ // Can this token be refreshed?
+ refreshable: boolean;
+}
diff --git a/packages/demobank-ui/src/index.html b/packages/demobank-ui/src/index.html
index e21e1fccc..315985648 100644
--- a/packages/demobank-ui/src/index.html
+++ b/packages/demobank-ui/src/index.html
@@ -16,27 +16,28 @@
@author Sebastian Javier Marchano
-->
<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="taler-support" content="uri">
- <meta name="mobile-web-app-capable" content="yes" />
- <meta name="apple-mobile-web-app-capable" content="yes" />
- <link
- rel="icon"
- href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
- />
- <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
- <title>Demobank</title>
- <!-- Optional customization script. -->
- <script src="demobank-ui-settings.js"></script>
- <!-- Entry point for the demobank SPA. -->
- <script type="module" src="index.js"></script>
- <link rel="stylesheet" href="index.css" />
- </head>
- <body>
- <div id="app"></div>
- </body>
-</html>
+<html lang="en" class="h-full bg-gray-100">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri">
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Demobank</title>
+ <!-- Optional customization script. -->
+ <script src="demobank-ui-settings.js"></script>
+ <!-- Entry point for the demobank SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+</head>
+
+<body class="h-full">
+ <div id="app"></div>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/demobank-ui/src/index.tsx b/packages/demobank-ui/src/index.tsx
index 2e0f740fe..b7d69fd2d 100644
--- a/packages/demobank-ui/src/index.tsx
+++ b/packages/demobank-ui/src/index.tsx
@@ -16,7 +16,7 @@
import App from "./components/app.js";
import { h, render } from "preact";
-import "./scss/main.scss";
+import "./scss/main.css"
const app = document.getElementById("app");
diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx b/packages/demobank-ui/src/pages/AccountPage.tsx
deleted file mode 100644
index 820c59984..000000000
--- a/packages/demobank-ui/src/pages/AccountPage.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- 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 { Amounts, HttpStatusCode, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
-import {
- ErrorType,
- HttpResponsePaginated,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { Transactions } from "../components/Transactions/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { useAccountDetails } from "../hooks/access.js";
-import { LoginForm } from "./LoginForm.js";
-import { PaymentOptions } from "./PaymentOptions.js";
-import { notifyError } from "../hooks/notification.js";
-import { useEffect, useState } from "preact/hooks";
-
-interface Props {
- account: string;
- onLoadNotOk: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => VNode;
-}
-
-export const CopyIcon = (): VNode => (
- <svg height="16" viewBox="0 0 16 16" width="16">
- <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>
-);
-
-export const CopiedIcon = (): VNode => (
- <svg height="16" viewBox="0 0 16 16" width="16">
- <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>
-);
-
-function CopyButton({ getContent }: { 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 onClick={copyText} style={{width:32, height:32, fontSize: "initial"}}>
- <CopyIcon />
- </button>
- );
- }
- return (
- <div content="Copied" style={{display:"inline-block"}}>
- <button disabled style={{width:32, height:32 , fontSize: "initial"}}>
- <CopiedIcon />
- </button>
- </div>
- );
-}
-
-
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-export function AccountPage({ account, onLoadNotOk }: Props): VNode {
- const result = useAccountDetails(account);
- const backend = useBackendContext();
- const { i18n } = useTranslationContext();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- //logout if there is any error, not if loading
- backend.logOut();
- if (result.status === HttpStatusCode.NotFound) {
- notifyError({
- title: i18n.str`Username or account label "${account}" not found`,
- });
- return <LoginForm />;
- }
- return onLoadNotOk(result);
- }
-
- const { data } = result;
- const balance = Amounts.parseOrThrow(data.balance.amount);
- const debitThreshold = Amounts.parseOrThrow(data.debitThreshold);
- const payto = parsePaytoUri(data.paytoUri);
- if (!payto || !payto.isKnown || payto.targetType !== "iban") {
- return (
- <div>Payto from server is not valid &quot;{data.paytoUri}&quot;</div>
- );
- }
- const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
- const limit = balanceIsDebit
- ? Amounts.sub(debitThreshold, balance).amount
- : Amounts.add(balance, debitThreshold).amount;
- return (
- <Fragment>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>
- Welcome, {account} (<a href={stringifyPaytoUri(payto)}>{payto.iban}</a>)! <CopyButton getContent={() => stringifyPaytoUri(payto)} />
- </i18n.Translate>
- </h1>
- </div>
-
- <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>
- <section id="payments">
- <div class="payments">
- <h2>{i18n.str`Payments`}</h2>
- <PaymentOptions limit={limit} />
- </div>
- </section>
-
- <section style={{ marginTop: "2em" }}>
- <div class="active">
- <h3>{i18n.str`Latest transactions`}</h3>
- <Transactions account={account} />
- </div>
- </section>
- </Fragment>
- );
-}
diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts
new file mode 100644
index 000000000..9230fb6b1
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AccountPage/index.ts
@@ -0,0 +1,92 @@
+/*
+ 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 { HttpError, HttpResponseOk, HttpResponsePaginated, utils } from "@gnu-taler/web-util/browser";
+import { AbsoluteTime, AmountJson, PaytoUriIBAN, PaytoUriTalerBank } from "@gnu-taler/taler-util";
+import { Loading } from "../../components/Loading.js";
+import { useComponentState } from "./state.js";
+import { ReadyView, InvalidIbanView } from "./views.js";
+import { VNode } from "preact";
+import { LoginForm } from "../LoginForm.js";
+import { ErrorLoading } from "../../components/ErrorLoading.js";
+
+export interface Props {
+ account: string;
+ onLoadNotOk: <T>(
+ error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+ ) => VNode;
+ goToBusinessAccount: () => void;
+ goToConfirmOperation: (id: string) => void;
+}
+
+export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingError {
+ status: "loading-error";
+ error: HttpError<SandboxBackend.SandboxError>;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ account: string,
+ limit: AmountJson,
+ goToBusinessAccount: () => void;
+ goToConfirmOperation: (id: string) => void;
+ }
+
+ export interface InvalidIban {
+ status: "invalid-iban",
+ error: HttpResponseOk<SandboxBackend.CoreBank.AccountData>;
+ }
+
+ export interface UserNotFound {
+ status: "error-user-not-found",
+ error: HttpError<any>;
+ onRegister?: () => void;
+ }
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "error-user-not-found": LoginForm,
+ "invalid-iban": InvalidIbanView,
+ "loading-error": ErrorLoading,
+ ready: ReadyView,
+};
+
+export const AccountPage = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts
new file mode 100644
index 000000000..ca7e1d447
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AccountPage/state.ts
@@ -0,0 +1,92 @@
+/*
+ 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 { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
+import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useBackendContext } from "../../context/backend.js";
+import { useAccountDetails } from "../../hooks/access.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({ account, goToBusinessAccount, goToConfirmOperation }: Props): State {
+ const result = useAccountDetails(account);
+ const backend = useBackendContext();
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ if (!result.ok) {
+ if (result.loading || result.type === ErrorType.TIMEOUT) {
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+ //logout if there is any error, not if loading
+ // backend.logOut();
+ if (result.status === HttpStatusCode.NotFound) {
+ notifyError(i18n.str`Username or account label "${account}" not found`, undefined);
+ return {
+ status: "error-user-not-found",
+ error: result,
+ };
+ }
+ if (result.status === HttpStatusCode.Unauthorized) {
+ notifyError(i18n.str`Authorization denied`, i18n.str`Maybe the session has expired, login again.`);
+ return {
+ status: "error-user-not-found",
+ error: result,
+ };
+ }
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+
+ const { data } = result;
+
+ const balance = Amounts.parseOrThrow(data.balance.amount);
+
+ const debitThreshold = Amounts.parseOrThrow(data.debit_threshold);
+ const payto = parsePaytoUri(data.payto_uri);
+
+ if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) {
+ return {
+ status: "invalid-iban",
+ error: result
+ };
+ }
+
+ const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
+ const limit = balanceIsDebit
+ ? Amounts.sub(debitThreshold, balance).amount
+ : Amounts.add(balance, debitThreshold).amount;
+
+
+ return {
+ status: "ready",
+ goToBusinessAccount,
+ goToConfirmOperation,
+ error: undefined,
+ account,
+ limit,
+ };
+}
diff --git a/packages/demobank-ui/src/scss/_footer.scss b/packages/demobank-ui/src/pages/AccountPage/stories.tsx
index 112522ed8..f3828a5d6 100644
--- a/packages/demobank-ui/src/scss/_footer.scss
+++ b/packages/demobank-ui/src/pages/AccountPage/stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (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
@@ -19,17 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-footer.footer {
- .logo {
- img {
- width: auto;
- height: $footer-logo-height;
- }
- }
-}
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
-@include mobile {
- .footer-copyright {
- text-align: center;
- }
-}
+export default {
+ title: "account page",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/demobank-ui/src/scss/_mixins.scss b/packages/demobank-ui/src/pages/AccountPage/test.ts
index b52e590e3..588b84c35 100644
--- a/packages/demobank-ui/src/scss/_mixins.scss
+++ b/packages/demobank-ui/src/pages/AccountPage/test.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (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
@@ -19,16 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-@mixin transition($t) {
- transition: $t 250ms ease-in-out 50ms;
-}
+import * as tests from "@gnu-taler/web-util/testing";
+import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
-@mixin icon-with-update-mark($icon-base-width) {
- .icon {
- width: $icon-base-width;
-
- &.has-update-mark:after {
- right: ($icon-base-width / 2) - 0.85;
- }
- }
-}
+describe("Account states", () => {
+ it("should do some tests", async () => {
+ });
+});
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx
new file mode 100644
index 000000000..483cb579a
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -0,0 +1,93 @@
+/*
+ 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Attention } from "../../components/Attention.js";
+import { Transactions } from "../../components/Transactions/index.js";
+import { useBusinessAccountDetails } from "../../hooks/circuit.js";
+import { useSettings } from "../../hooks/settings.js";
+import { PaymentOptions } from "../PaymentOptions.js";
+import { State } from "./index.js";
+
+export function InvalidIbanView({ error }: State.InvalidIban) {
+ return (
+ <div>Payto from server is not valid &quot;{error.data.payto_uri}&quot;</div>
+ );
+}
+
+const IS_PUBLIC_ACCOUNT_ENABLED = false
+
+function ShowDemoInfo(): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings();
+ if (!settings.showDemoDescription) return <Fragment />
+ return <Attention title={i18n.str`This is a demo bank`} onClose={() => {
+ updateSettings("showDemoDescription", false);
+ }}>
+ {IS_PUBLIC_ACCOUNT_ENABLED ? (
+ <i18n.Translate>
+ This part of the demo shows how a bank that supports Taler
+ directly would work. In addition to using your own bank
+ account, you can also see the transaction history of some{" "}
+ <a href="/public-accounts">Public Accounts</a>.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ This part of the demo shows how a bank that supports Taler
+ directly would work.
+ </i18n.Translate>
+ )}
+ </Attention>
+}
+
+export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> {
+ const { i18n } = useTranslationContext();
+
+ return <Fragment>
+ <MaybeBusinessButton account={account} onClick={goToBusinessAccount} />
+
+ <ShowDemoInfo />
+
+ <PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} />
+ <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/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx
deleted file mode 100644
index ce0feebce..000000000
--- a/packages/demobank-ui/src/pages/AdminPage.tsx
+++ /dev/null
@@ -1,1064 +0,0 @@
-/*
- 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 { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
-import {
- ErrorType,
- HttpResponsePaginated,
- RequestError,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Cashouts } from "../components/Cashouts/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { useAccountDetails } from "../hooks/access.js";
-import {
- useAdminAccountAPI,
- useBusinessAccountDetails,
- useBusinessAccounts,
-} from "../hooks/circuit.js";
-import {
- buildRequestErrorMessage,
- PartialButDefined,
- RecursivePartial,
- undefinedIfEmpty,
- validateIBAN,
- WithIntermediate,
-} from "../utils.js";
-import { ErrorBannerFloat } from "./BankFrame.js";
-import { ShowCashoutDetails } from "./BusinessAccount.js";
-import { handleNotOkResult } from "./HomePage.js";
-import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { ErrorMessage, notifyInfo } from "../hooks/notification.js";
-
-const charset =
- "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
-const upperIdx = charset.indexOf("A");
-
-function randomPassword(): string {
- const random = Array.from({ length: 16 }).map(() => {
- return charset.charCodeAt(Math.random() * charset.length);
- });
- // first char can't be upper
- const charIdx = charset.indexOf(String.fromCharCode(random[0]));
- random[0] =
- charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0];
- return String.fromCharCode(...random);
-}
-
-interface Props {
- onRegister: () => void;
-}
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-export function AdminPage({ onRegister }: Props): VNode {
- const [account, setAccount] = useState<string | undefined>();
- const [showDetails, setShowDetails] = useState<string | undefined>();
- const [showCashouts, setShowCashouts] = useState<string | undefined>();
- const [updatePassword, setUpdatePassword] = useState<string | undefined>();
- const [removeAccount, setRemoveAccount] = useState<string | undefined>();
- const [showCashoutDetails, setShowCashoutDetails] = useState<
- string | undefined
- >();
-
- const [createAccount, setCreateAccount] = useState(false);
-
- const result = useBusinessAccounts({ account });
- const { i18n } = useTranslationContext();
-
- if (result.loading) return <div />;
- if (!result.ok) {
- return handleNotOkResult(i18n, onRegister)(result);
- }
-
- const { customers } = result.data;
-
- if (showCashoutDetails) {
- return (
- <ShowCashoutDetails
- id={showCashoutDetails}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onCancel={() => {
- setShowCashoutDetails(undefined);
- }}
- />
- );
- }
-
- if (showCashouts) {
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Cashout for account {showCashouts}</i18n.Translate>
- </h1>
- </div>
- <Cashouts
- account={showCashouts}
- onSelected={(id) => {
- setShowCashouts(id);
- setShowCashouts(undefined);
- }}
- />
- <p>
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- setShowCashouts(undefined);
- }}
- />
- </p>
- </div>
- );
- }
-
- if (showDetails) {
- return (
- <ShowAccountDetails
- account={showDetails}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onChangePassword={() => {
- setUpdatePassword(showDetails);
- setShowDetails(undefined);
- }}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account updated`);
- setShowDetails(undefined);
- }}
- onClear={() => {
- setShowDetails(undefined);
- }}
- />
- );
- }
- if (removeAccount) {
- return (
- <RemoveAccount
- account={removeAccount}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account removed`);
- setRemoveAccount(undefined);
- }}
- onClear={() => {
- setRemoveAccount(undefined);
- }}
- />
- );
- }
- if (updatePassword) {
- return (
- <UpdateAccountPassword
- account={updatePassword}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Password changed`);
- setUpdatePassword(undefined);
- }}
- onClear={() => {
- setUpdatePassword(undefined);
- }}
- />
- );
- }
- if (createAccount) {
- return (
- <CreateNewAccount
- onClose={() => setCreateAccount(false)}
- onCreateSuccess={(password) => {
- notifyInfo(
- i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
- );
- setCreateAccount(false);
- }}
- />
- );
- }
-
- 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>
-
- <AdminAccount onRegister={onRegister} />
- <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();
- setShowDetails(item.username);
- }}
- >
- {item.username}
- </a>
- </td>
- <td>{item.name}</td>
- <td>
- {!balance ? (
- i18n.str`unknown`
- ) : (
- <span class="amount">
- {balanceIsDebit ? <b>-</b> : null}
- <span class="value">{`${Amounts.stringifyValue(
- balance,
- )}`}</span>
- &nbsp;
- <span class="currency">{`${balance.currency}`}</span>
- </span>
- )}
- </td>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setUpdatePassword(item.username);
- }}
- >
- change password
- </a>
- &nbsp;
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setShowCashouts(item.username);
- }}
- >
- cashouts
- </a>
- &nbsp;
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setRemoveAccount(item.username);
- }}
- >
- remove
- </a>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- </article>
- )}
- </section>
- </Fragment>
- );
-}
-
-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!`);
- }}
- />
- </Fragment>
- );
-}
-
-const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
-const EMAIL_REGEX =
- /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
-
-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;
-}
-
-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>();
- const [error, saveError] = useState<ErrorMessage | 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>;
- }
- 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>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
-
- <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>
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
- }}
- />
- </div>
- <div>
- <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) {
- saveError(buildRequestErrorMessage(i18n, error.cause));
- } else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
- </div>
- </div>
- </p>
- </div>
- </div>
- );
-}
-
-function CreateNewAccount({
- onClose,
- onCreateSuccess,
-}: {
- onClose: () => void;
- onCreateSuccess: (password: string) => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { createAccount } = useAdminAccountAPI();
- const [submitAccount, setSubmitAccount] = useState<
- SandboxBackend.Circuit.CircuitAccountData | undefined
- >();
- const [error, saveError] = useState<ErrorMessage | undefined>();
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>New account</i18n.Translate>
- </h1>
- </div>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
-
- <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
- <AccountForm
- template={undefined}
- purpose="create"
- onChange={(a) => {
- setSubmitAccount(a);
- }}
- />
-
- <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();
-
- 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: randomPassword(),
- };
-
- await createAccount(account);
- onCreateSuccess(account.password);
- } catch (error) {
- if (error instanceof RequestError) {
- saveError(
- 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 {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
- </div>
- </div>
- </p>
- </div>
- </div>
- );
-}
-
-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
- >();
- const [error, saveError] = useState<ErrorMessage | 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>;
- }
- return onLoadNotOk(result);
- }
-
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Business account details</i18n.Translate>
- </h1>
- </div>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
- <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>
- {onClear ? (
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
- }}
- />
- ) : undefined}
- </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) {
- saveError(
- 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 {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }
- }}
- />
- </div>
- </div>
- </div>
- </p>
- </div>
- </div>
- );
-}
-
-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();
- const [error, saveError] = useState<ErrorMessage | 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>;
- }
- return onLoadNotOk(result);
- }
-
- 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);
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Remove account: {account}</i18n.Translate>
- </h1>
- </div>
- {!isBalanceEmpty && (
- <ErrorBannerFloat
- error={{
- title: i18n.str`Can't delete the account`,
- description: i18n.str`Balance is not empty`,
- }}
- onClear={() => saveError(undefined)}
- />
- )}
- {error && (
- <ErrorBannerFloat error={error} 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>
- <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) {
- saveError(
- 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 {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
- </div>
- </div>
- </p>
- </div>
- );
-}
-/**
- * Create valid account object to update or create
- * Take template as initial values for the form
- * Purpose indicate if all field al read only (show), part of them (update)
- * or none (create)
- * @param param0
- * @returns
- */
-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
- ? 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`
- : 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>
- );
-}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index dc61f1302..6ab6ba3e4 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -14,283 +14,362 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Logger, TranslatedString } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { StateUpdater, useEffect, useState } from "preact/hooks";
-import talerLogo from "../assets/logo-white.svg";
-import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
+import { Amounts, Logger, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util";
+import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import logo from "../assets/logo-2021.svg";
+import { Attention } from "../components/Attention.js";
+import { CopyButton } from "../components/CopyButton.js";
+import { LangSelector } from "../components/LangSelector.js";
import { useBackendContext } from "../context/backend.js";
-import { useBusinessAccountDetails } from "../hooks/circuit.js";
-import { bankUiSettings } from "../settings.js";
+import { useAccountDetails } from "../hooks/access.js";
import { useSettings } from "../hooks/settings.js";
-import { ErrorMessage, onNotificationUpdate } from "../hooks/notification.js";
+import { bankUiSettings } from "../settings.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
-const IS_PUBLIC_ACCOUNT_ENABLED = false;
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
const versionText = VERSION
? GIT_HASH
- ? `Version ${VERSION} (${GIT_HASH.substring(0, 8)})`
+ ? <a href={`https://git.taler.net/wallet-core.git/tree/?id=${GIT_HASH}`} target="_blank" rel="noreferrer noopener">
+ Version {VERSION} ({GIT_HASH.substring(0, 8)})
+ </a>
: 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,
children: ComponentChildren;
- goToBusinessAccount?: () => void;
}): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendContext();
const [settings, updateSettings] = useSettings();
+ const [open, setOpen] = useState(false)
- const demo_sites = [];
- for (const i in bankUiSettings.demoSites)
- demo_sites.push(
- <a href={bankUiSettings.demoSites[i][1]}>
- {bankUiSettings.demoSites[i][0]}
- </a>,
- );
+ const [error, resetError] = useErrorBoundary();
- return (
- <Fragment>
- <header
- class="demobar"
- style="display: flex; flex-direction: row; justify-content: space-between;"
- >
- <a href="#main" class="skip">{i18n.str`Skip to main content`}</a>
- <div style="max-width: 50em; margin-left: 2em; margin-right: 2em;">
- <h1>
- <span class="it">
- <a href="/">{bankUiSettings.bankName}</a>
- </span>
- </h1>
- {maybeDemoContent(
- <p>
- {IS_PUBLIC_ACCOUNT_ENABLED ? (
- <i18n.Translate>
- This part of the demo shows how a bank that supports Taler
- directly would work. In addition to using your own bank
- account, you can also see the transaction history of some{" "}
- <a href="/public-accounts">Public Accounts</a>.
- </i18n.Translate>
- ) : (
- <i18n.Translate>
- This part of the demo shows how a bank that supports Taler
- directly would work.
- </i18n.Translate>
- )}
- </p>,
- )}
- </div>
- </header>
- <div style="display:flex; flex-direction: column;" class="navcontainer">
- <nav class="demolist">
- {maybeDemoContent(<Fragment>{demo_sites}</Fragment>)}
- {backend.state.status === "loggedIn" ? (
- <Fragment>
- {goToBusinessAccount && !backend.state.isUserAdministrator ? (
- <MaybeBusinessButton
- account={backend.state.username}
- onClick={goToBusinessAccount}
- />
- ) : undefined}
-
- <LangSelector />
-
- <a
- href="#"
- class="pure-button logout-button"
- onClick={() => {
- backend.logOut();
- updateSettings("currentWithdrawalOperationId", undefined);
- }}
- >{i18n.str`Logout`}</a>
- </Fragment>
- ) : undefined}
- </nav>
- </div>
- <section id="main" class="content">
- <StatusBanner />
- {children}
- </section>
- <section id="footer" class="footer">
- <hr />
- <div>
- <p>
- You can learn more about GNU Taler on our{" "}
- <a href="https://taler.net">main website</a>.
- </p>
- </div>
- <div style="flex-grow:1" />
- <p>
- Copyright &copy; 2014&mdash;2022 Taler Systems SA. {versionText}{" "}
- <TestingTag />
- </p>
- </section>
- </Fragment>
- );
-}
+ useEffect(() => {
+ if (error) {
+ const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString
+ if (error instanceof Error) {
+ notifyException(i18n.str`Internal error, please report.`, error)
+ } else {
+ notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString)
+ }
+ resetError()
+ }
+ }, [error])
-function maybeDemoContent(content: VNode): VNode {
- if (bankUiSettings.showDemoNav) {
- return content;
+ const demo_sites = [];
+ if (bankUiSettings.demoSites) {
+ for (const i in bankUiSettings.demoSites)
+ demo_sites.push(
+ <a href={bankUiSettings.demoSites[i][1]}>
+ {bankUiSettings.demoSites[i][0]}
+ </a>,
+ );
}
- return <Fragment />;
-}
-export function ErrorBannerFloat({
- error,
- onClear,
-}: {
- error: ErrorMessage;
- onClear?: () => void;
-}): VNode {
- return (
- <div
- style={{
- position: "fixed",
- top: 10,
- zIndex: 200,
- width: "90%",
- }}
- >
- <ErrorBanner error={error} onClear={onClear} />
- </div>
- );
-}
+ return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;">
+ <div class="bg-indigo-600 pb-32">
+ <nav class="">
+ <div class="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8">
+ <div class="relative flex h-16 items-center justify-between ">
+ <div class="flex items-center px-2 lg:px-0">
+ <div class="flex-shrink-0 bg-white rounded-lg">
+ <a href={bankUiSettings.iconLinkURL ?? "#"}>
+ <img
+ class="h-8 w-auto"
+ src={logo}
+ alt="Taler"
+ style={{ height: "1.5rem", margin: ".5rem" }}
+ />
+ </a>
+ </div>
+ {bankUiSettings.demoSites &&
+ <div class="hidden sm:block lg:ml-10 ">
+ <div class="flex space-x-4">
+ {/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
+ {bankUiSettings.demoSites.map(([name, url]) => {
+ return <a href={url} class="text-white hover:bg-indigo-500 hover:bg-opacity-75 rounded-md py-2 px-3 text-sm font-medium">{name}</a>
+ })}
+ </div>
+ </div>
+ }
+ </div>
-function ErrorBanner({
- error,
- onClear,
-}: {
- error: ErrorMessage;
- onClear?: () => void;
-}): VNode {
- return (
- <div
- class="informational informational-fail"
- style={{
- marginTop: 8,
- paddingLeft: 16,
- paddingRight: 16,
- }}
- >
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <p>
- <b>{error.title}</b>
- </p>
- <div style={{ marginTop: "auto", marginBottom: "auto" }}>
- {onClear && (
- <input
- type="button"
- class="pure-button"
- value="Clear"
- onClick={(e) => {
- e.preventDefault();
- onClear();
- }}
- />
- )}
+ <div class="flex">
+ <button type="button" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 p-1 text-indigo-200 hover:bg-indigo-500 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false"
+ onClick={(e) => {
+ setOpen(!open)
+ }}>
+ <span class="absolute -inset-0.5"></span>
+ <span class="sr-only">Open settings</span>
+ <svg class="block h-10 w-10" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
+ </svg>
+ </button>
+ </div>
+ </div>
</div>
- </div>
- <p>{error.description}</p>
- </div>
- );
-}
-function StatusBanner(): VNode | null {
- const [info, setInfo] = useState<TranslatedString>();
- const [error, setError] = useState<ErrorMessage>();
- useEffect(() => {
- return onNotificationUpdate((newValue) => {
- if (newValue === undefined) {
- setInfo(undefined);
- setError(undefined);
- } else {
- if (newValue.type === "error") {
- setError(newValue.error);
- } else {
- setInfo(newValue.info);
+ {open &&
+ <div class="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true"
+ onClick={() => {
+ setOpen(false)
+ }}>
+ <div class="fixed inset-0"></div>
+
+ <div class="fixed inset-0 overflow-hidden">
+ <div class="absolute inset-0 overflow-hidden">
+ <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
+ <div class="pointer-events-auto w-screen max-w-md" >
+ <div class="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl" onClick={(e) => {
+ //do not trigger close if clicking inside the sidebar
+ e.stopPropagation();
+ }}>
+ <div class="px-4 sm:px-6" >
+ <div class="flex items-start justify-between" >
+ <h2 class="text-base font-semibold leading-6 text-gray-900" id="slide-over-title">
+ <i18n.Translate>Preferences</i18n.Translate>
+ </h2>
+ <div class="ml-3 flex h-7 items-center">
+ <button type="button" class="relative rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
+ onClick={(e) => {
+ setOpen(false)
+ }}
+
+ >
+ <span class="absolute -inset-2.5"></span>
+ <span class="sr-only">
+ <i18n.Translate>Close panel</i18n.Translate>
+ </span>
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
+ </svg>
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="relative mt-6 flex-1 px-4 sm:px-6">
+ <nav class="flex flex-1 flex-col" aria-label="Sidebar">
+ <ul role="list" class="flex flex-1 flex-col gap-y-7">
+ <li>
+ <a href="#"
+ class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
+ onClick={() => {
+ backend.logOut();
+ setOpen(false)
+ updateSettings("currentWithdrawalOperationId", undefined);
+ }}
+ >
+ <svg class="h-6 w-6 shrink-0 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
+ </svg>
+ <i18n.Translate>Log out</i18n.Translate>
+ </a>
+ </li>
+ <li>
+ <LangSelector />
+ </li>
+ {bankUiSettings.demoSites &&
+ <li class="sm:hidden">
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Sites</i18n.Translate>
+ </div>
+ <ul role="list" class="-mx-2 mt-2 space-y-1">
+ {bankUiSettings.demoSites.map(([name, url]) => {
+ return <li>
+ <a href={url} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-indigo-600 hover:bg-gray-100 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
+ <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border text-[0.625rem] font-medium bg-white text-gray-400 border-gray-200 group-hover:border-indigo-600 group-hover:text-indigo-600">&gt;</span>
+ <span class="truncate">{name}</span>
+ </a>
+ </li>
+ })}
+ </ul>
+ </li>
+ }
+ <li>
+ <ul role="list" class="space-y-1">
+ <li class="mt-2">
+ <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>Show withdrawal confirmation</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={settings.showWithdrawalSuccess} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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={() => {
+ updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess);
+ }}>
+ <span aria-hidden="true" data-enabled={settings.showWithdrawalSuccess} class="translate-x-5 data-[enabled=false]: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>
+ </li>
+ <li class="mt-2">
+ <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>Show demo description</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={settings.showDemoDescription} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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={() => {
+ updateSettings("showDemoDescription", !settings.showDemoDescription);
+ }}>
+ <span aria-hidden="true" data-enabled={settings.showDemoDescription} class="translate-x-5 data-[enabled=false]: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>
+ </li>
+ <li class="mt-2">
+ <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>Show debug info</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={settings.showDebugInfo} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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={() => {
+ updateSettings("showDebugInfo", !settings.showDebugInfo);
+ }}>
+ <span aria-hidden="true" data-enabled={settings.showDebugInfo} class="translate-x-5 data-[enabled=false]: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>
+ </li>
+ <li class="mt-2">
+ <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>Show install wallet first</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={settings.showInstallWallet} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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={() => {
+ updateSettings("showInstallWallet", !settings.showInstallWallet);
+ }}>
+ <span aria-hidden="true" data-enabled={settings.showInstallWallet} class="translate-x-5 data-[enabled=false]: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>
+ </li>
+ <li class="mt-2">
+ <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>Use fast withdrawal</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={settings.fastWithdrawal} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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={() => {
+ updateSettings("fastWithdrawal", !settings.fastWithdrawal);
+ }}>
+ <span aria-hidden="true" data-enabled={settings.fastWithdrawal} class="translate-x-5 data-[enabled=false]: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>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
}
- }
- });
- }, []);
- return (
- <div
- style={{
- position: "fixed",
- top: 10,
- zIndex: 200,
- width: "90%",
- }}
- >
- {!info ? undefined : (
- <div
- class="informational informational-ok"
- style={{ marginTop: 8, paddingLeft: 16, paddingRight: 16 }}
- >
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <p>
- <b>{info}</b>
- </p>
- <div>
- <input
- type="button"
- class="pure-button"
- value="Clear"
- onClick={async () => {
- setInfo(undefined);
- }}
- />
+ </nav >
+
+ {account &&
+ <header class="py-5 border-t border-indigo-300 border-opacity-25 bg-indigo-600 lg:border-t lg:border-indigo-400 lg:border-opacity-25">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+ <div class=" flex flex-wrap items-center justify-between sm:flex-nowrap">
+ <h3 class="text-2xl font-bold tracking-tight text-white"><WelcomeAccount account={account} /></h3>
+ <div>
+ <h3 class="text-2xl font-bold tracking-tight text-white"><AccountBalance account={account} /></h3>
+ </div>
</div>
</div>
+
+ </header>
+ }
+ </div >
+
+ <StatusBanner />
+ <main class="-mt-32 flex-1">
+ <div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
+ <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
+ {children}
</div>
- )}
- {!error ? undefined : (
- <ErrorBanner
- error={error}
- onClear={() => {
- setError(undefined);
- }}
- />
- )}
- </div>
+ </div>
+ </main>
+
+ <Footer />
+ </div >
+
);
}
+function MaybeShowDebugInfo({ info }: { info: any }): VNode {
+ const [settings] = useSettings()
+ if (settings.showDebugInfo) {
+ return <pre class="whitespace-break-spaces ">
+ {info}
+ </pre>
+ }
+ return <Fragment />
+}
+
+
+function StatusBanner(): VNode {
+ const notifs = useNotifications()
+ if (notifs.length === 0) return <Fragment />
+ return <div class="fixed z-20 w-full p-4"> {
+ notifs.map(n => {
+ switch (n.message.type) {
+ case "error":
+ return <Attention type="danger" title={n.message.title} onClose={() => {
+ n.remove()
+ }}>
+ {n.message.description &&
+ <div class="mt-2 text-sm text-red-700">
+ {n.message.description}
+ </div>
+ }
+ <MaybeShowDebugInfo info={n.message.debug} />
+ {/* <a href="#" class="text-gray-500">
+ show debug info
+ </a>
+ {n.message.debug &&
+ <div class="mt-2 text-sm text-red-700 font-mono break-all">
+ {n.message.debug}
+ </div>
+ } */}
+ </Attention>
+ case "info":
+ return <Attention type="success" title={n.message.title} onClose={() => {
+ n.remove();
+ }} />
+ }
+ })}
+ </div>
+
+}
+
function TestingTag(): VNode {
const testingUrl = localStorage.getItem("bank-base-url");
if (!testingUrl) return <Fragment />;
return (
- <span style={{ color: "gray" }}>
+ <p class="text-xs leading-5 text-gray-300">
Testing with {testingUrl}{" "}
<a
href=""
@@ -302,6 +381,58 @@ function TestingTag(): VNode {
>
stop testing
</a>
- </span>
+ </p>
+ );
+}
+
+function Footer() {
+ const { i18n } = useTranslationContext()
+ return (
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">
+ <i18n.Translate>
+ Learn more about <a target="_blank" rel="noreferrer noopener" class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net">GNU Taler</a>
+ </i18n.Translate>
+ </p>
+ </div>
+ <div style="flex-grow:1" />
+ <p class="text-xs leading-5 text-gray-400">
+ Copyright &copy; 2014&mdash;2023 Taler Systems SA. {versionText}{" "}
+ <TestingTag />
+ </p>
+ </div>
+ </footer>
);
}
+
+function WelcomeAccount({ account }: { account: string }): VNode {
+ const { i18n } = useTranslationContext();
+
+ const result = useAccountDetails(account);
+ if (!result.ok) return <div />
+
+ const payto = parsePaytoUri(result.data.payto_uri)
+ if (!payto) return <div />
+
+ const accountNumber = !payto.isKnown ? undefined : payto.targetType === "iban" ? payto.iban : payto.targetType === "x-taler-bank" ? payto.account : undefined;
+ return <i18n.Translate>
+ Welcome, {account} {accountNumber !== undefined ?
+ <span>
+ (<a href={result.data.payto_uri}>{accountNumber}</a> <CopyButton getContent={() => result.data.payto_uri} />)
+ </span>
+ : <Fragment />}!
+ </i18n.Translate>
+
+}
+
+function AccountBalance({ account }: { account: string }): VNode {
+ const result = useAccountDetails(account);
+ if (!result.ok) return <div />
+
+ return <RenderAmount
+ value={Amounts.parseOrThrow(result.data.balance.amount)}
+ negative={result.data.balance.credit_debit_indicator === "debit"}
+ />
+}
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx
index 93a9bdfae..95144f086 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -17,6 +17,7 @@
import {
HttpStatusCode,
Logger,
+ TranslatedString,
parseWithdrawUri,
stringifyWithdrawUri,
} from "@gnu-taler/taler-util";
@@ -24,18 +25,18 @@ import {
ErrorType,
HttpResponse,
HttpResponsePaginated,
+ notify,
+ notifyError,
useTranslationContext,
} 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 { notifyError, notifyInfo } from "../hooks/notification.js";
import { useSettings } from "../hooks/settings.js";
-import { AccountPage } from "./AccountPage.js";
-import { AdminPage } from "./AdminPage.js";
+import { AccountPage } from "./AccountPage/index.js";
import { LoginForm } from "./LoginForm.js";
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
+import { route } from "preact-router";
const logger = new Logger("AccountPage");
@@ -51,73 +52,66 @@ const logger = new Logger("AccountPage");
*/
export function HomePage({
onRegister,
- onPendingOperationFound,
+ account,
+ goToConfirmOperation,
+ goToBusinessAccount,
}: {
- onPendingOperationFound: (id: string) => void;
+ account: string,
onRegister: () => void;
+ goToBusinessAccount: () => void;
+ goToConfirmOperation: (id: string) => void;
}): VNode {
- const backend = useBackendContext();
- const [settings] = useSettings();
const { i18n } = useTranslationContext();
- if (backend.state.status === "loggedOut") {
- return <LoginForm onRegister={onRegister} />;
- }
-
- if (settings.currentWithdrawalOperationId) {
- onPendingOperationFound(settings.currentWithdrawalOperationId);
- return <Loading />;
- }
-
- if (backend.state.isUserAdministrator) {
- return <AdminPage onRegister={onRegister} />;
- }
-
return (
<AccountPage
- account={backend.state.username}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ account={account}
+ goToConfirmOperation={goToConfirmOperation}
+ goToBusinessAccount={goToBusinessAccount}
+ onLoadNotOk={handleNotOkResult(i18n)}
/>
);
}
export function WithdrawalOperationPage({
operationId,
- onLoadNotOk,
onContinue,
}: {
operationId: string;
- onLoadNotOk: () => void;
onContinue: () => void;
}): VNode {
//FIXME: libeufin sandbox should return show to create the integration api endpoint
//or return withdrawal uri from response
+ const baseUrl = getInitialBackendBaseURL()
const uri = stringifyWithdrawUri({
- bankIntegrationApiBaseUrl: `${getInitialBackendBaseURL()}/integration-api`,
+ bankIntegrationApiBaseUrl: `${baseUrl}/taler-integration`,
withdrawalOperationId: operationId,
});
const parsedUri = parseWithdrawUri(uri);
const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings();
if (!parsedUri) {
- notifyError({
- title: i18n.str`The Withdrawal URI is not valid: "${uri}"`,
- });
+ notifyError(
+ i18n.str`The Withdrawal URI is not valid`,
+ uri as TranslatedString
+ );
return <Loading />;
}
return (
<WithdrawalQRCode
withdrawUri={parsedUri}
- onContinue={onContinue}
- onLoadNotOk={onLoadNotOk}
+ onClose={() => {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onContinue()
+ }}
/>
);
}
export function handleNotOkResult(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
- onRegister?: () => void,
): <T>(
result:
| HttpResponsePaginated<T, SandboxBackend.SandboxError>
@@ -125,53 +119,53 @@ export function handleNotOkResult(
) => VNode {
return function handleNotOkResult2<T>(
result:
- | HttpResponsePaginated<T, SandboxBackend.SandboxError>
- | HttpResponse<T, SandboxBackend.SandboxError>,
+ | HttpResponsePaginated<T, SandboxBackend.SandboxError | undefined>
+ | HttpResponse<T, SandboxBackend.SandboxError | undefined>,
): VNode {
if (result.loading) return <Loading />;
if (!result.ok) {
switch (result.type) {
case ErrorType.TIMEOUT: {
- notifyError({
- title: i18n.str`Request timeout, try again later.`,
- });
+ notifyError(i18n.str`Request timeout, try again later.`, undefined);
break;
}
case ErrorType.CLIENT: {
if (result.status === HttpStatusCode.Unauthorized) {
- notifyError({
- title: i18n.str`Wrong credentials`,
- });
- return <LoginForm onRegister={onRegister} />;
+ notifyError(i18n.str`Wrong credentials`, undefined);
+ return <LoginForm />;
}
const errorData = result.payload;
- notifyError({
- title: i18n.str`Could not load due to a client error`,
- description: errorData.error.description,
+ notify({
+ type: "error",
+ title: i18n.str`Could not load due to a request error`,
+ description: i18n.str`Request to url "${result.info.url}" returned ${result.info.status}`,
debug: JSON.stringify(result),
});
break;
}
case ErrorType.SERVER: {
- notifyError({
+ notify({
+ type: "error",
title: i18n.str`Server returned with error`,
- description: result.payload.error.description,
+ description: result.payload?.error?.description as TranslatedString,
debug: JSON.stringify(result.payload),
});
break;
}
case ErrorType.UNREADABLE: {
- notifyError({
+ notify({
+ type: "error",
title: i18n.str`Unexpected error.`,
- description: `Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
+ description: i18n.str`Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
debug: JSON.stringify(result),
});
break;
}
case ErrorType.UNEXPECTED: {
- notifyError({
+ notify({
+ type: "error",
title: i18n.str`Unexpected error.`,
- description: `Diagnostic from ${result.info?.url} is "${result.message}"`,
+ description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`,
debug: JSON.stringify(result),
});
break;
@@ -180,7 +174,7 @@ export function handleNotOkResult(
assertUnreachable(result);
}
}
-
+ // route("/")
return <div>error</div>;
}
return <div />;
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index d2cb1bd8e..3ea94b899 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,199 +14,249 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode } from "@gnu-taler/taler-util";
-import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { ErrorType, 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 { useBackendContext } from "../context/backend.js";
-import { useCredentialsChecker } from "../hooks/backend.js";
-import { ErrorMessage } from "../hooks/notification.js";
+import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
import { bankUiSettings } from "../settings.js";
import { undefinedIfEmpty } from "../utils.js";
-import { ErrorBannerFloat } from "./BankFrame.js";
-import { USERNAME_REGEX } from "./RegistrationPage.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+
/**
* Collect and submit login data.
*/
export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
const backend = useBackendContext();
- const [username, setUsername] = useState<string | undefined>();
+ const currentUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined
+ const [username, setUsername] = useState<string | undefined>(currentUser);
const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
- const testLogin = useCredentialsChecker();
- const [error, saveError] = useState<ErrorMessage | undefined>();
+ const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
+
+
+ /**
+ * Register form may be shown in the initialization step.
+ * If this is an error when usgin the app the registration
+ * callback is not set
+ */
+ const isSessionExpired = !onRegister
+
+ // useEffect(() => {
+ // if (backend.state.status === "loggedIn") {
+ // backend.expired()
+ // }
+ // },[])
const ref = useRef<HTMLInputElement>(null);
useEffect(function focusInput() {
+ //FIXME: show invalidate session and allow relogin
+ if (isSessionExpired) {
+ localStorage.removeItem("backend-state");
+ window.location.reload()
+ }
ref.current?.focus();
}, []);
+ const [busy, setBusy] = useState<Record<string, undefined>>()
const errors = undefinedIfEmpty({
username: !username
? i18n.str`Missing username`
- : !USERNAME_REGEX.test(username)
- ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ // : !USERNAME_REGEX.test(username)
+ // ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
: undefined,
password: !password ? i18n.str`Missing password` : undefined,
- });
+ }) ?? busy;
+
+ function saveError({ title, description, debug }: { title: TranslatedString, description?: TranslatedString, debug?: any }) {
+ notifyError(title, description, debug)
+ }
+
+ async function doLogout() {
+ backend.logOut()
+ }
+
+ async function doLogin() {
+ if (!username || !password) return;
+ setBusy({})
+ const result = await requestNewLoginToken(username, password);
+ if (result.valid) {
+ backend.logIn({ username, token: result.token });
+ } else {
+ const { cause } = result;
+ switch (cause.type) {
+ case ErrorType.CLIENT: {
+ if (cause.status === HttpStatusCode.Unauthorized) {
+ saveError({
+ title: i18n.str`Wrong credentials for "${username}"`,
+ });
+ } else
+ if (cause.status === HttpStatusCode.NotFound) {
+ saveError({
+ title: i18n.str`Account not found`,
+ });
+ } else {
+ saveError({
+ title: i18n.str`Could not load due to a request error`,
+ description: i18n.str`Request to url "${cause.info.url}" returned ${cause.info.status}`,
+ debug: JSON.stringify(cause.payload),
+ });
+ }
+ break;
+ }
+ case ErrorType.SERVER: {
+ saveError({
+ title: i18n.str`Server had a problem, try again later or report.`,
+ // description: cause.payload.error.description,
+ debug: JSON.stringify(cause.payload),
+ });
+ break;
+ }
+ case ErrorType.TIMEOUT: {
+ saveError({
+ title: i18n.str`Request timeout, try again later.`,
+ });
+ break;
+ }
+ case ErrorType.UNREADABLE: {
+ saveError({
+ title: i18n.str`Unexpected error.`,
+ description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}` as TranslatedString,
+ debug: JSON.stringify(cause),
+ });
+ break;
+ }
+ default: {
+ saveError({
+ title: i18n.str`Unexpected error, please report.`,
+ description: `Diagnostic from ${cause.info?.url} is "${cause.message}"` as TranslatedString,
+ debug: JSON.stringify(cause),
+ });
+ break;
+ }
+ }
+ // backend.logOut();
+ }
+ setPassword(undefined);
+ setBusy(undefined)
+ }
return (
- <Fragment>
- <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
- <div class="login-div">
- <form
- class="login-form"
- noValidate
+ <div class="flex min-h-full flex-col justify-center">
+
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
+ <form class="space-y-6" noValidate
onSubmit={(e) => {
e.preventDefault();
}}
autoCapitalize="none"
autoCorrect="off"
>
- <div class="pure-form">
- <h2>{i18n.str`Please login!`}</h2>
- <p class="unameFieldLabel loginFieldLabel formFieldLabel">
- <label for="username">{i18n.str`Username:`}</label>
- </p>
- <input
- ref={ref}
- autoFocus
- type="text"
- name="username"
- id="username"
- value={username ?? ""}
- enterkeyhint="next"
- placeholder="Username"
- autocomplete="username"
- required
- onInput={(e): void => {
- setUsername(e.currentTarget.value);
+ <div>
+ <label for="username" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Username</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ ref={doAutoFocus}
+ type="text"
+ name="username"
+ id="username"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={username ?? ""}
+ disabled={isSessionExpired}
+ enterkeyhint="next"
+ placeholder="identification"
+ autocomplete="username"
+ required
+ onInput={(e): void => {
+ setUsername(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.username}
+ isDirty={username !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
+ </div>
+ <div class="mt-2">
+ <input
+ type="password"
+ name="password"
+ id="password"
+ autocomplete="current-password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ enterkeyhint="send"
+ value={password ?? ""}
+ placeholder="Password"
+ required
+ onInput={(e): void => {
+ setPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
+ </div>
+
+ {isSessionExpired ? <div class="flex justify-between">
+ <button type="submit"
+ class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
+ onClick={(e) => {
+ e.preventDefault()
+ doLogout()
}}
- />
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={username !== undefined}
- />
- <p class="passFieldLabel loginFieldLabel formFieldLabel">
- <label for="password">{i18n.str`Password:`}</label>
- </p>
- <input
- type="password"
- name="password"
- id="password"
- autocomplete="current-password"
- enterkeyhint="send"
- value={password ?? ""}
- placeholder="Password"
- required
- onInput={(e): void => {
- setPassword(e.currentTarget.value);
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <button type="submit"
+ class="rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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()
+ doLogin()
}}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- <br />
- <button
- type="submit"
- class="pure-button pure-button-primary"
+ >
+ <i18n.Translate>Renew session</i18n.Translate>
+ </button>
+ </div> : <div>
+ <button type="submit"
+ class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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={async (e) => {
- e.preventDefault();
- if (!username || !password) return;
- const testResult = await testLogin(username, password);
- if (testResult.valid) {
- backend.logIn({ username, password });
- } else {
- if (testResult.requestError) {
- const { cause } = testResult;
- switch (cause.type) {
- case ErrorType.CLIENT: {
- if (cause.status === HttpStatusCode.Unauthorized) {
- saveError({
- title: i18n.str`Wrong credentials for "${username}"`,
- });
- }
- if (cause.status === HttpStatusCode.NotFound) {
- saveError({
- title: i18n.str`Account not found`,
- });
- } else {
- saveError({
- title: i18n.str`Could not load due to a client error`,
- description: cause.payload.error.description,
- debug: JSON.stringify(cause.payload),
- });
- }
- break;
- }
- case ErrorType.SERVER: {
- saveError({
- title: i18n.str`Server had a problem, try again later or report.`,
- description: cause.payload.error.description,
- debug: JSON.stringify(cause.payload),
- });
- break;
- }
- case ErrorType.TIMEOUT: {
- saveError({
- title: i18n.str`Request timeout, try again later.`,
- });
- break;
- }
- case ErrorType.UNREADABLE: {
- saveError({
- title: i18n.str`Unexpected error.`,
- description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}`,
- debug: JSON.stringify(cause),
- });
- break;
- }
- default: {
- saveError({
- title: i18n.str`Unexpected error, please report.`,
- description: `Diagnostic from ${cause.info?.url} is "${cause.message}"`,
- debug: JSON.stringify(cause),
- });
- break;
- }
- }
- } else {
- saveError({
- title: i18n.str`Unexpected error, please report.`,
- debug: JSON.stringify(testResult.error),
- });
- }
- backend.logOut();
- }
- setUsername(undefined);
- setPassword(undefined);
+ onClick={(e) => {
+ e.preventDefault()
+ doLogin()
}}
>
- {i18n.str`Login`}
+ <i18n.Translate>Log in</i18n.Translate>
</button>
-
- {bankUiSettings.allowRegistrations && onRegister ? (
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={(e) => {
- e.preventDefault();
- onRegister();
- }}
- >
- {i18n.str`Register`}
- </button>
- ) : (
- <div />
- )}
- </div>
+ </div>}
</form>
+
+ {bankUiSettings.allowRegistrations && onRegister &&
+ <p class="mt-10 text-center text-sm text-gray-500 border-t">
+ <button type="submit"
+ class="flex mt-4 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
+ onClick={(e) => {
+ e.preventDefault()
+ onRegister()
+ }}
+ >
+ <i18n.Translate>Register</i18n.Translate>
+ </button>
+ </p>
+ }
</div>
- </Fragment>
+ </div>
);
}
diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts
new file mode 100644
index 000000000..b347fd942
--- /dev/null
+++ b/packages/demobank-ui/src/pages/OperationState/index.ts
@@ -0,0 +1,122 @@
+/*
+ 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 { AbsoluteTime, AmountJson, WithdrawUriResult } from "@gnu-taler/taler-util";
+import { HttpError, utils } from "@gnu-taler/web-util/browser";
+import { ErrorLoading } from "../../components/ErrorLoading.js";
+import { Loading } from "../../components/Loading.js";
+import { useComponentState } from "./state.js";
+import { AbortedView, ConfirmedView, InvalidPaytoView, InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView, ReadyView } from "./views.js";
+
+export interface Props {
+ currency: string;
+ onClose: () => void;
+}
+
+export type State = State.Loading |
+ State.LoadingError |
+ State.Ready |
+ State.Aborted |
+ State.Confirmed |
+ State.InvalidPayto |
+ State.InvalidWithdrawal |
+ State.InvalidReserve |
+ State.NeedConfirmation;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingError {
+ status: "loading-error";
+ error: HttpError<SandboxBackend.SandboxError>;
+ }
+
+ /**
+ * Need to open the wallet
+ */
+ export interface Ready {
+ status: "ready";
+ error: undefined;
+ uri: WithdrawUriResult,
+ onClose: () => void;
+ onAbort: () => void;
+ }
+
+ export interface InvalidPayto {
+ status: "invalid-payto",
+ error: undefined;
+ payto: string | null;
+ onClose: () => void;
+ }
+ export interface InvalidWithdrawal {
+ status: "invalid-withdrawal",
+ error: undefined;
+ onClose: () => void;
+ uri: string,
+ }
+ export interface InvalidReserve {
+ status: "invalid-reserve",
+ error: undefined;
+ onClose: () => void;
+ reserve: string | null;
+ }
+ export interface NeedConfirmation {
+ status: "need-confirmation",
+ onAbort: () => void;
+ onConfirm: () => void;
+ error: undefined;
+ busy: boolean,
+ }
+ export interface Aborted {
+ status: "aborted",
+ error: undefined;
+ onClose: () => void;
+ }
+ export interface Confirmed {
+ status: "confirmed",
+ error: undefined;
+ onClose: () => void;
+ }
+
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "invalid-payto": InvalidPaytoView,
+ "invalid-withdrawal": InvalidWithdrawalView,
+ "invalid-reserve": InvalidReserveView,
+ "need-confirmation": NeedConfirmationView,
+ "aborted": AbortedView,
+ "confirmed": ConfirmedView,
+ "loading-error": ErrorLoading,
+ ready: ReadyView,
+};
+
+export const OperationState = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts
new file mode 100644
index 000000000..4be680377
--- /dev/null
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -0,0 +1,265 @@
+/*
+ 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 { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { RequestError, notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { useAccessAPI, useAccessAnonAPI, useWithdrawalDetails } from "../../hooks/access.js";
+import { getInitialBackendBaseURL } from "../../hooks/backend.js";
+import { useSettings } from "../../hooks/settings.js";
+import { buildRequestErrorMessage } from "../../utils.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({ currency, onClose }: Props): utils.RecursiveState<State> {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+ const { createWithdrawal } = useAccessAPI();
+ const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI();
+ const [busy, setBusy] = useState<Record<string, undefined>>()
+
+ const amount = settings.maxWithdrawalAmount
+
+ async function doSilentStart() {
+ //FIXME: if amount is not enough use balance
+ const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
+
+ try {
+ const result = await createWithdrawal({
+ amount: Amounts.stringify(parsedAmount),
+ });
+ const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
+ if (!uri) {
+ return notifyError(
+ i18n.str`Server responded with an invalid withdraw URI`,
+ i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
+ } else {
+ updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The operation was rejected due to insufficient funds`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
+ const withdrawalOperationId = settings.currentWithdrawalOperationId
+ useEffect(() => {
+ if (withdrawalOperationId === undefined) {
+ doSilentStart()
+ }
+ }, [settings.fastWithdrawal, amount])
+
+ const baseUrl = getInitialBackendBaseURL()
+
+ if (!withdrawalOperationId) {
+ return {
+ status: "loading",
+ error: undefined
+ }
+ }
+
+ const wid = withdrawalOperationId
+
+ async function doAbort() {
+ try {
+ setBusy({})
+ await abortWithdrawal(wid);
+ onClose();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+
+ async function doConfirm() {
+ try {
+ setBusy({})
+ await confirmWithdrawal(wid);
+ if (!settings.showWithdrawalSuccess) {
+ notifyInfo(i18n.str`Wire transfer completed!`)
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
+ : status === HttpStatusCode.UnprocessableEntity
+ ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+ const bankIntegrationApiBaseUrl = `${baseUrl}/taler-integration`
+ const uri = stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl,
+ withdrawalOperationId,
+ });
+ const parsedUri = parseWithdrawUri(uri);
+ if (!parsedUri) {
+ return {
+ status: "invalid-withdrawal",
+ error: undefined,
+ uri,
+ onClose,
+ }
+ }
+
+ return (): utils.RecursiveState<State> => {
+ const result = useWithdrawalDetails(withdrawalOperationId);
+ const shouldCreateNewOperation = !result.ok && !result.loading && result.info.status === HttpStatusCode.NotFound
+
+ useEffect(() => {
+ if (shouldCreateNewOperation) {
+ doSilentStart()
+ }
+ }, [])
+ if (!result.ok) {
+ if (result.loading) {
+ return {
+ status: "loading",
+ error: undefined
+ }
+ }
+ if (result.info.status === HttpStatusCode.NotFound) {
+ return {
+ status: "loading",
+ error: undefined,
+ }
+ }
+ return {
+ status: "loading-error",
+ error: result
+ }
+ }
+ const { data } = result;
+ if (data.aborted) {
+ return {
+ status: "aborted",
+ error: undefined,
+ onClose: async () => {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ }
+ }
+
+ if (data.confirmation_done) {
+ if (!settings.showWithdrawalSuccess) {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ }
+ return {
+ status: "confirmed",
+ error: undefined,
+ onClose: async () => {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ }
+ }
+
+ if (!data.selection_done) {
+ return {
+ status: "ready",
+ error: undefined,
+ uri: parsedUri,
+ onClose: async () => {
+ await doAbort()
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ onAbort: doAbort,
+ }
+ }
+
+ if (!data.selected_reserve_pub) {
+ return {
+ status: "invalid-reserve",
+ error: undefined,
+ reserve: data.selected_reserve_pub,
+ onClose,
+ }
+ }
+
+ const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account)
+
+ if (!account) {
+ return {
+ status: "invalid-payto",
+ error: undefined,
+ payto: data.selected_exchange_account,
+ onClose,
+ }
+ }
+
+
+ // goToConfirmOperation(withdrawalOperationId)
+ return {
+ status: "need-confirmation",
+ error: undefined,
+ onAbort: async () => {
+ await doAbort()
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ busy: !!busy,
+ onConfirm: doConfirm
+ }
+ }
+
+}
diff --git a/packages/demobank-ui/src/scss/_tiles.scss b/packages/demobank-ui/src/pages/OperationState/stories.tsx
index e69d995f0..03917a8fb 100644
--- a/packages/demobank-ui/src/scss/_tiles.scss
+++ b/packages/demobank-ui/src/pages/OperationState/stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (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
@@ -19,6 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-.is-tiles-wrapper {
- margin-bottom: $default-padding;
-}
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "operation status page",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/demobank-ui/src/scss/_modal.scss b/packages/demobank-ui/src/pages/OperationState/test.ts
index b3a31ebf1..f4d6cf4b2 100644
--- a/packages/demobank-ui/src/scss/_modal.scss
+++ b/packages/demobank-ui/src/pages/OperationState/test.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (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
@@ -19,17 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-.modal-card {
- width: $modal-card-width;
-}
+import * as tests from "@gnu-taler/web-util/testing";
+import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
-.modal-card-foot {
- background-color: $modal-card-foot-background-color;
-}
-
-@include mobile {
- .modal .animation-content .modal-card {
- width: $modal-card-width-mobile;
- margin: 0 auto;
- }
-}
+describe("Withdrawal operation states", () => {
+ it("should do some tests", async () => {
+ });
+});
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx
new file mode 100644
index 000000000..2cb7385db
--- /dev/null
+++ b/packages/demobank-ui/src/pages/OperationState/views.tsx
@@ -0,0 +1,376 @@
+/*
+ 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 { stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useMemo, useState } from "preact/hooks";
+import { QR } from "../../components/QR.js";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { useSettings } from "../../hooks/settings.js";
+import { undefinedIfEmpty } from "../../utils.js";
+import { State } from "./index.js";
+
+export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) {
+ return (
+ <div>Payto from server is not valid &quot;{payto}&quot;</div>
+ );
+}
+export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) {
+ return (
+ <div>Withdrawal uri from server is not valid &quot;{uri}&quot;</div>
+ );
+}
+export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) {
+ return (
+ <div>Reserve from server is not valid &quot;{reserve}&quot;</div>
+ );
+}
+
+export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State.NeedConfirmation) {
+ const { i18n } = useTranslationContext()
+
+ const captchaNumbers = useMemo(() => {
+ return {
+ a: Math.floor(Math.random() * 10),
+ b: Math.floor(Math.random() * 10),
+ };
+ }, []);
+ const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
+ const answer = parseInt(captchaAnswer ?? "", 10);
+ const errors = undefinedIfEmpty({
+ answer: !captchaAnswer
+ ? i18n.str`Answer the question before continue`
+ : Number.isNaN(answer)
+ ? i18n.str`The answer should be a number`
+ : answer !== captchaNumbers.a + captchaNumbers.b
+ ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
+ : undefined,
+ }) ?? (busy ? {} as Record<string, undefined> : undefined);
+
+ return (
+ <div class="bg-white shadow sm:rounded-lg">
+ <div class="px-4 py-5 sm:p-6">
+ <h3 class="text-base font-semibold text-gray-900">
+ <i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
+ </h3>
+ <div class="mt-2 max-w-xl text-sm text-gray-500">
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-4 sm:gap-x-3">
+
+ <label class={"relative sm:col-span-2 flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 ring-indigo-600"}>
+ <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" />
+ <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>challenge response test</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600" 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-gray-100 p-4 shadow-sm focus:outline-none 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" />
+ <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>using SMS</i18n.Translate>
+ </span>
+ <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>not available</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600 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-gray-100 p-4 shadow-sm focus:outline-none 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" />
+ <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>one time password</i18n.Translate>
+ </span>
+ <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>not available</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600 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>
+ </div>
+ <div class="mt-3 text-sm leading-6">
+
+ <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">
+ <label for="withdraw-amount">{i18n.str`What is`}&nbsp;
+ <em>
+ {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
+ </em>
+ ?
+ </label>
+ <div class="mt-2">
+ <div class="relative rounded-md shadow-sm">
+ <input
+ type="text"
+ // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ aria-describedby="answer"
+ autoFocus
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={captchaAnswer ?? ""}
+ required
+
+ name="answer"
+ id="answer"
+ autocomplete="off"
+ onChange={(e): void => {
+ setCaptchaAnswer(e.currentTarget.value)
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} />
+ </div>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onAbort}
+ >
+ <i18n.Translate>Cancel</i18n.Translate></button>
+ <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()
+ onConfirm()
+ }}
+ >
+ <i18n.Translate>Transfer</i18n.Translate>
+ </button>
+ </div>
+
+ </form>
+ </div>
+ <div class="px-4 mt-4 ">
+ {/* <div class="w-full">
+ <div class="px-4 sm:px-0 text-sm">
+ <p><i18n.Translate>Wire transfer details</i18n.Translate></p>
+ </div>
+ <div class="mt-6 border-t border-gray-100">
+ <dl class="divide-y divide-gray-100">
+ {((): VNode => {
+ switch (details.account.targetType) {
+ case "iban": {
+ const p = details.account as PaytoUriIBAN
+ const name = p.params["receiver-name"]
+ return <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
+ </div>
+ {name &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
+ }
+ case "x-taler-bank": {
+ const p = details.account as PaytoUriTalerBank
+ const name = p.params["receiver-name"]
+ return <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
+ </div>
+ {name &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
+ }
+ default:
+ return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
+ </div>
+
+ }
+ })()}
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Withdrawal identification</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0 break-words">{details.reserve}</dd>
+ </div>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">To be added</dd>
+ // {/* Amounts.stringifyValue(details.amount)
+ </div>
+ </dl>
+ </div>
+ </div> */}
+
+ </div>
+ </div>
+ </div>
+
+ );
+}
+export function AbortedView({ error, onClose }: State.Aborted) {
+ return (
+ <div>aborted</div>
+ );
+}
+
+export function ConfirmedView({ error, onClose }: State.Confirmed) {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+ return (
+ <Fragment>
+
+ <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white p-4 text-left shadow-xl transition-all ">
+
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
+ <svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
+ </svg>
+ </div>
+ <div class="mt-3 text-center sm:mt-5">
+ <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
+ <i18n.Translate>Withdrawal confirmed</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <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>Do not show this again</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={!settings.showWithdrawalSuccess} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 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={() => {
+ updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess);
+ }}>
+ <span aria-hidden="true" data-enabled={!settings.showWithdrawalSuccess} class="translate-x-5 data-[enabled=false]: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>
+ <div class="mt-5 sm:mt-6">
+ <button type="button"
+ class="inline-flex w-full justify-center 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={async (e) => {
+ e.preventDefault();
+ onClose()
+ }}>
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ </div>
+ </Fragment>
+
+ );
+}
+
+export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> {
+ const { i18n } = useTranslationContext();
+
+ useEffect(() => {
+ //Taler Wallet WebExtension is listening to headers response and tab updates.
+ //In the SPA there is no header response with the Taler URI so
+ //this hack manually triggers the tab update after the QR is in the DOM.
+ // WebExtension will be using
+ // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
+ document.title = `${document.title} ${uri.withdrawalOperationId}`;
+ }, []);
+ const talerWithdrawUri = stringifyWithdrawUri(uri);
+ return <Fragment>
+ <div class="flex justify-end mt-4">
+ <button type="button"
+ class="inline-flex items-center 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-500"
+ onClick={() => {
+ onClose()
+ }}
+ >
+ Cancel
+ </button>
+ </div>
+
+ <div class="bg-white shadow sm:rounded-lg mt-4">
+ <div class="p-4">
+ <h3 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>On this device</i18n.Translate>
+ </h3>
+ <div class="mt-2 sm:flex sm:items-start sm:justify-between">
+ <div class="max-w-xl text-sm text-gray-500">
+ <p>
+ <i18n.Translate>If you are using a desktop browser you can open the popup now or click the link if you have the "Inject Taler support" option enabled.</i18n.Translate>
+ </p>
+ </div>
+ <div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center">
+ <a href={talerWithdrawUri}
+ class="inline-flex items-center 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"
+ >
+ <i18n.Translate>Start</i18n.Translate>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="bg-white shadow sm:rounded-lg mt-2">
+ <div class="p-4">
+ <h3 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>On a mobile phone</i18n.Translate>
+ </h3>
+ <div class="mt-2 sm:flex sm:items-start sm:justify-between">
+ <div class="max-w-xl text-sm text-gray-500">
+ <p>
+ <i18n.Translate>Scan the QR code with your mobile device.</i18n.Translate>
+ </p>
+ </div>
+ </div>
+ <div class="mt-2 max-w-md ml-auto mr-auto">
+ <QR text={talerWithdrawUri} />
+ </div>
+ </div>
+ </div>
+
+ </Fragment>
+
+}
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 3552da7b4..f60ba3270 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -15,10 +15,9 @@
*/
import { AmountJson } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { notifyInfo } from "../hooks/notification.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
import { useSettings } from "../hooks/settings.js";
@@ -27,60 +26,97 @@ import { useSettings } from "../hooks/settings.js";
* Let the user choose a payment option,
* then specify the details trigger the action.
*/
-export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
+export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJson, goToConfirmOperation: (id: string) => void }): VNode {
const { i18n } = useTranslationContext();
- const [settings, updateSettings] = useSettings();
+ const [settings] = useSettings();
- const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
- "charge-wallet",
- );
+ const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
return (
- <article>
- <div class="payments">
- <div class="tab">
- <button
- class={tab === "charge-wallet" ? "tablinks active" : "tablinks"}
- onClick={(): void => {
- setTab("charge-wallet");
- }}
- >
- {i18n.str`Withdraw `}
- </button>
- <button
- class={tab === "wire-transfer" ? "tablinks active" : "tablinks"}
- onClick={(): void => {
- setTab("wire-transfer");
- }}
- >
- {i18n.str`Wire transfer`}
- </button>
+ <div class="mt-2">
+
+ <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">
+ <div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
+ <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>
+ {!!settings.currentWithdrawalOperationId &&
+ <span class="inline-flex items-center gap-x-1.5 w-fit rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">
+ <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true">
+ <circle cx="3" cy="3" r="3" />
+ </svg>
+ <i18n.Translate>operation ready</i18n.Translate>
+ </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>
+
+
+ <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">
+ <div class="text-4xl mr-4 my-auto">&#x2194;</div>
+ <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 bank account number</i18n.Translate>
+ </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" && (
- <div id="charge-wallet" class="tabcontent active">
- <h3>{i18n.str`Obtain digital cash`}</h3>
- <WalletWithdrawForm
- focus
- limit={limit}
- onSuccess={(id) => {
- updateSettings("currentWithdrawalOperationId", id);
- }}
- />
- </div>
+ <WalletWithdrawForm
+ focus
+ limit={limit}
+ goToConfirmOperation={goToConfirmOperation}
+ onCancel={() => {
+ setTab(undefined)
+ }}
+ />
)}
{tab === "wire-transfer" && (
- <div id="wire-transfer" class="tabcontent active">
- <h3>{i18n.str`Transfer to bank account`}</h3>
- <PaytoWireTransferForm
- focus
- limit={limit}
- onSuccess={() => {
- notifyInfo(i18n.str`Wire transfer created!`);
- }}
- />
- </div>
+ <PaytoWireTransferForm
+ focus
+ title={i18n.str`Transfer details`}
+ limit={limit}
+ onSuccess={() => {
+ notifyInfo(i18n.str`Wire transfer created!`);
+ setTab(undefined)
+ }}
+ onCancel={() => {
+ setTab(undefined)
+ }}
+ />
)}
- </div>
- </article>
- );
+
+ </fieldset>
+ </div>
+ )
}
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index d8c1644b1..52dbd4ff6 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -17,42 +17,51 @@
import {
AmountJson,
Amounts,
- buildPayto,
HttpStatusCode,
Logger,
+ TranslatedString,
+ buildPayto,
parsePaytoUri,
- stringifyPaytoUri,
+ stringifyPaytoUri
} from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { h, VNode, Fragment, Ref } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
-import { notifyError } from "../hooks/notification.js";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAccessAPI } from "../hooks/access.js";
import {
buildRequestErrorMessage,
undefinedIfEmpty,
validateIBAN,
} from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { useConfigState } from "../hooks/config.js";
+import { useConfigContext } from "../context/config.js";
const logger = new Logger("PaytoWireTransferForm");
export function PaytoWireTransferForm({
focus,
+ title,
onSuccess,
+ onCancel,
limit,
}: {
+ title: TranslatedString,
focus?: boolean;
onSuccess: () => void;
+ onCancel: (() => void) | undefined;
limit: AmountJson;
}): VNode {
const [isRawPayto, setIsRawPayto] = useState(false);
- const [iban, setIban] = useState<string | undefined>(undefined);
- const [subject, setSubject] = useState<string | undefined>(undefined);
- const [amount, setAmount] = useState<string | undefined>(undefined);
+ // FIXME: remove this
+ const [iban, setIban] = useState<string | undefined>();
+ const [subject, setSubject] = useState<string | undefined>();
+ const [amount, setAmount] = useState<string | undefined>();
const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
undefined,
@@ -70,295 +79,372 @@ export function PaytoWireTransferForm({
const errorsWire = undefinedIfEmpty({
iban: !iban
- ? i18n.str`Missing IBAN`
+ ? i18n.str`required`
: !IBAN_REGEX.test(iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(iban, i18n),
- subject: !subject ? i18n.str`Missing subject` : undefined,
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : validateIBAN(iban, i18n),
+ subject: !subject ? i18n.str`required` : undefined,
amount: !trimmedAmountStr
- ? i18n.str`Missing amount`
+ ? i18n.str`required`
: !parsedAmount
- ? i18n.str`Amount is not valid`
- : Amounts.isZero(parsedAmount)
- ? i18n.str`Should be greater than 0`
- : Amounts.cmp(limit, parsedAmount) === -1
- ? i18n.str`balance is not enough`
- : undefined,
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedAmount)
+ ? i18n.str`should be greater than 0`
+ : Amounts.cmp(limit, parsedAmount) === -1
+ ? i18n.str`balance is not enough`
+ : undefined,
});
const { createTransaction } = useAccessAPI();
- if (!isRawPayto)
- return (
- <div>
- <form
- class="pure-form"
- name="wire-transfer-form"
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <label for="iban">{i18n.str`Receiver IBAN:`}</label>&nbsp;
- <input
- ref={ref}
- type="text"
- id="iban"
- name="iban"
- value={iban ?? ""}
- placeholder="CC0123456789"
- required
- pattern={ibanRegex}
- onInput={(e): void => {
- setIban(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errorsWire?.iban}
- isDirty={iban !== undefined}
- />
- <label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
- <input
- type="text"
- name="subject"
- id="subject"
- placeholder="subject"
- value={subject ?? ""}
- required
- onInput={(e): void => {
- setSubject(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errorsWire?.subject}
- isDirty={subject !== undefined}
- />
- <label for="amount">{i18n.str`Amount:`}</label>&nbsp;
- <div style={{ width: "max-content", display: "flex" }}>
- <input
- type="text"
- readonly
- class="currency-indicator"
- size={limit.currency.length}
- maxLength={limit.currency.length}
- tabIndex={-1}
- style={{
- borderTopRightRadius: 0,
- borderBottomRightRadius: 0,
- borderRight: 0,
- }}
- value={limit.currency}
- />
- <input
- type="number"
- name="amount"
- id="amount"
- placeholder="amount"
- required
- style={{
- borderTopLeftRadius: 0,
- borderBottomLeftRadius: 0,
- borderLeft: 0,
- width: 150,
- }}
- value={amount ?? ""}
- onInput={(e): void => {
- setAmount(e.currentTarget.value);
- }}
- />
- </div>
- <ShowInputErrorLabel
- message={errorsWire?.amount}
- isDirty={amount !== undefined}
- />
- <p style={{ display: "flex", justifyContent: "space-between" }}>
- <input
- type="submit"
- class="pure-button pure-button-primary"
- disabled={!!errorsWire}
- value="Send"
- onClick={async (e) => {
- e.preventDefault();
- if (!(iban && subject && amount)) {
- return;
- }
- const ibanPayto = buildPayto("iban", iban, undefined);
- ibanPayto.params.message = encodeURIComponent(subject);
- const paytoUri = stringifyPaytoUri(ibanPayto);
-
- try {
- await createTransaction({
- paytoUri,
- amount: `${limit.currency}:${amount}`,
- });
- onSuccess();
- setAmount(undefined);
- setIban(undefined);
- setSubject(undefined);
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.BadRequest
- ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
- <input
- type="button"
- class="pure-button"
- value="Clear"
- onClick={async (e) => {
- e.preventDefault();
- setAmount(undefined);
- setIban(undefined);
- setSubject(undefined);
- }}
- />
- </p>
- </form>
- <p>
- <a
- href="#"
- onClick={(e) => {
- setIsRawPayto(true);
- e.preventDefault();
- }}
- >
- {i18n.str`Want to try the raw payto://-format?`}
- </a>
- </p>
- </div>
- );
-
const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
const errorsPayto = undefinedIfEmpty({
rawPaytoInput: !rawPaytoInput
? i18n.str`required`
: !parsed
- ? i18n.str`does not follow the pattern`
- : !parsed.params.amount
- ? i18n.str`use the "amount" parameter to specify the amount to be transferred`
- : Amounts.parse(parsed.params.amount) === undefined
- ? i18n.str`the amount is not valid`
- : !parsed.params.message
- ? i18n.str`use the "message" parameter to specify a reference text for the transfer`
- : !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),
+ ? i18n.str`does not follow the pattern`
+ : !parsed.isKnown || parsed.targetType !== "iban"
+ ? i18n.str`only "IBAN" target are supported`
+ : !parsed.params.amount
+ ? i18n.str`use the "amount" parameter to specify the amount to be transferred`
+ : Amounts.parse(parsed.params.amount) === undefined
+ ? i18n.str`the amount is not valid`
+ : !parsed.params.message
+ ? i18n.str`use the "message" parameter to specify a reference text for the transfer`
+ : !IBAN_REGEX.test(parsed.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : validateIBAN(parsed.iban, i18n),
});
- return (
- <div>
- <p>{i18n.str`Transfer money to account identified by payto:// URI:`}</p>
- <form
- class="pure-form"
- name="payto-form"
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <p>
- <label for="address">{i18n.str`payto URI:`}</label>&nbsp;
- <input
- name="address"
- type="text"
- size={50}
- ref={ref}
- id="address"
- value={rawPaytoInput ?? ""}
- required
- placeholder={i18n.str`payto address`}
- // pattern={`payto://iban/[A-Z][A-Z][0-9]+?message=[a-zA-Z0-9 ]+&amount=${currency}:[0-9]+(.[0-9]+)?`}
- onInput={(e): void => {
- rawPaytoInputSetter(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errorsPayto?.rawPaytoInput}
- isDirty={rawPaytoInput !== undefined}
- />
- <br />
- <div style={{ fontSize: "small", marginTop: 4 }}>
- Hint:
- <code>
- payto://iban/[receiver-iban]?message=[subject]&amount=[
- {limit.currency}
- :X.Y]
- </code>
- </div>
- </p>
- <p>
- <input
- class="pure-button pure-button-primary"
- type="button"
- disabled={!!errorsPayto}
- value={i18n.str`Send`}
- onClick={async () => {
- if (!rawPaytoInput) {
- logger.error("Didn't get any raw Payto string!");
- return;
+ async function doSend() {
+ let payto_uri: string | undefined;
+
+ if (rawPaytoInput) {
+ payto_uri = rawPaytoInput
+ } else {
+ if (!iban || !subject) return;
+ const ibanPayto = buildPayto("iban", iban, undefined);
+ ibanPayto.params.message = encodeURIComponent(subject);
+ payto_uri = stringifyPaytoUri(ibanPayto);
+ }
+
+ try {
+ await createTransaction({
+ payto_uri,
+ amount: `${limit.currency}:${amount}`,
+ });
+ onSuccess();
+ setAmount(undefined);
+ setIban(undefined);
+ setSubject(undefined);
+ rawPaytoInputSetter(undefined)
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.BadRequest
+ ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
+ : undefined,
+ }),
+ );
+ } 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">
+ {/**
+ * FIXME: Scan a qr code
+ */}
+ <div class="px-4 sm:px-0">
+ <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")}>
+ <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" onChange={() => {
+ if (parsed && parsed.isKnown && parsed.targetType === "iban") {
+ setIban(parsed.iban)
+ const amount = Amounts.parse(parsed.params["amount"])
+ if (amount) {
+ setAmount(Amounts.stringifyValue(amount))
+ }
+ const subject = parsed.params["subject"]
+ if (subject) {
+ setSubject(subject)
+ }
}
+ setIsRawPayto(false)
+ }} />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Using a form</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ </label>
- try {
- await createTransaction({
- paytoUri: rawPaytoInput,
- });
- onSuccess();
- rawPaytoInputSetter(undefined);
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.BadRequest
- ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ <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")}>
+ <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" onChange={() => {
+ if (iban) {
+ const payto = buildPayto("iban", iban, undefined)
+ if (parsedAmount) {
+ payto.params["amount"] = Amounts.stringify(parsedAmount)
+ }
+ if (subject) {
+ payto.params["message"] = subject
}
+ rawPaytoInputSetter(stringifyPaytoUri(payto))
}
- }}
- />
- </p>
- <p>
- <a
- href="/account"
- onClick={() => {
- setIsRawPayto(false);
- }}
+ setIsRawPayto(true)
+ }} />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Import payto:// URI</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 w-fit mx-auto"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={e => {
+ e.preventDefault()
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ {!isRawPayto ?
+ <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+
+ <div class="sm:col-span-5">
+ <label for="iban" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Recipient`}</label>
+ <div class="mt-2">
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ 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 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ value={iban ?? ""}
+ placeholder="CC0123456789"
+ autocomplete="off"
+ required
+ pattern={ibanRegex}
+ onInput={(e): void => {
+ setIban(e.currentTarget.value.toUpperCase());
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsWire?.iban}
+ isDirty={iban !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>IBAN of the recipient's account</i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label for="subject" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Transfer subject`}</label>
+ <div class="mt-2">
+ <input
+ 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 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="subject"
+ id="subject"
+ autocomplete="off"
+ placeholder="subject"
+ value={subject ?? ""}
+ required
+ onInput={(e): void => {
+ setSubject(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsWire?.subject}
+ isDirty={subject !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >some text to identify the transfer</p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label for="amount" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Amount`}</label>
+ <InputAmount
+ name="amount"
+ left
+ currency={limit.currency}
+ value={trimmedAmountStr}
+ onChange={(d) => {
+ setAmount(d)
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsWire?.amount}
+ isDirty={subject !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500" >amount to transfer</p>
+ </div>
+
+ </div> :
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6 w-full">
+ <div class="sm:col-span-6">
+ <label for="address" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`payto URI:`}</label>
+ <div class="mt-2">
+ <textarea
+ ref={focus ? doAutoFocus : undefined}
+ name="address"
+ id="address"
+ type="textarea"
+ rows={3}
+ class="block overflow-hidden w-64 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={rawPaytoInput ?? ""}
+ required
+ placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`}
+ onInput={(e): void => {
+ rawPaytoInputSetter(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsPayto?.rawPaytoInput}
+ isDirty={rawPaytoInput !== undefined}
+ />
+ </div>
+ </div>
+ </div>
+ }
+ </div>
+ <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.str`Use wire-transfer form?`}
- </a>
- </p>
- </form>
+ <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={isRawPayto ? !!errorsPayto : !!errorsWire}
+ onClick={(e) => {
+ e.preventDefault()
+ doSend()
+ }}
+ >
+ <i18n.Translate>Send</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ </div >
+ )
+
+}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null) {
+ if (element) {
+ setTimeout(() => {
+ element.focus()
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center"
+ })
+ }, 100)
+ }
+}
+
+const FRAC_SEPARATOR = "."
+
+export function InputAmount(
+ {
+ currency,
+ name,
+ value,
+ error,
+ left,
+ onChange,
+ }: {
+ error?: string;
+ currency: string;
+ name: string;
+ left?: boolean | undefined,
+ value: string | undefined;
+ onChange?: (s: string) => void;
+ },
+ ref: Ref<HTMLInputElement>,
+): VNode {
+ const cfg = useConfigContext()
+ return (
+ <div class="mt-2">
+ <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
+ <div
+ class="pointer-events-none inset-y-0 flex items-center px-3"
+ >
+ <span class="text-gray-500 sm:text-sm">{currency}</span>
+ </div>
+ <input
+ type="number"
+ data-left={left}
+ class="text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6"
+ placeholder="0.00" aria-describedby="price-currency"
+ ref={ref}
+ name={name}
+ id={name}
+ autocomplete="off"
+ value={value ?? ""}
+ disabled={!onChange}
+ onInput={(e) => {
+ if (!onChange) return;
+ const l = e.currentTarget.value.length
+ const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR)
+ if (sep_pos !== -1 && l - sep_pos - 1 > cfg.currency_fraction_limit) {
+ e.currentTarget.value = e.currentTarget.value.substring(0, sep_pos + cfg.currency_fraction_limit + 1)
+ }
+ onChange(e.currentTarget.value);
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
</div>
);
}
+
+export function RenderAmount({ value, negative }: { value: AmountJson, negative?: boolean }): VNode {
+ const cfg = useConfigContext()
+ const str = Amounts.stringifyValue(value)
+ const sep_pos = str.indexOf(FRAC_SEPARATOR)
+ if (sep_pos !== -1 && str.length - sep_pos - 1 > cfg.currency_fraction_digits) {
+ const limit = sep_pos + cfg.currency_fraction_digits + 1
+ const normal = str.substring(0, limit)
+ const small = str.substring(limit)
+ return <span class="whitespace-nowrap">
+ {negative ? "-" : undefined}
+ {value.currency} {normal} <sup class="-ml-2">{small}</sup>
+ </span>
+ }
+ return <span class="whitespace-nowrap">
+ {negative ? "-" : undefined}
+ {value.currency} {str}
+ </span>
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
index 03bdb78b7..680368919 100644
--- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
@@ -36,8 +36,8 @@ export function PublicHistoriesPage({}: Props): VNode {
const result = usePublicAccounts();
const [showAccount, setShowAccount] = useState(
- result.ok && result.data.publicAccounts.length > 0
- ? result.data.publicAccounts[0].accountLabel
+ result.ok && result.data.public_accounts.length > 0
+ ? result.data.public_accounts[0].account_name
: undefined,
);
@@ -51,9 +51,9 @@ export function PublicHistoriesPage({}: Props): VNode {
const accountsBar = [];
// Ask story of all the public accounts.
- for (const account of data.publicAccounts) {
- logger.trace("Asking transactions for", account.accountLabel);
- const isSelected = account.accountLabel == showAccount;
+ for (const account of data.public_accounts) {
+ logger.trace("Asking transactions for", account.account_name);
+ const isSelected = account.account_name == showAccount;
accountsBar.push(
<li
class={
@@ -65,13 +65,13 @@ export function PublicHistoriesPage({}: Props): VNode {
<a
href="#"
class="pure-menu-link"
- onClick={() => setShowAccount(account.accountLabel)}
+ onClick={() => setShowAccount(account.account_name)}
>
- {account.accountLabel}
+ {account.account_name}
</a>
</li>,
);
- txs[account.accountLabel] = <Transactions account={account.accountLabel} />;
+ txs[account.account_name] = <Transactions account={account.account_name} />;
}
return (
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index c27984569..e07525ab4 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -17,17 +17,19 @@
import {
HttpStatusCode,
stringifyWithdrawUri,
+ TranslatedString,
WithdrawUriResult,
} from "@gnu-taler/taler-util";
import {
+ notify,
+ notifyError,
RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useEffect } from "preact/hooks";
import { QR } from "../components/QR.js";
import { useAccessAnonAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { buildRequestErrorMessage } from "../utils.js";
export function QrCodeSection({
@@ -49,47 +51,87 @@ export function QrCodeSection({
const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
const { abortWithdrawal } = useAccessAnonAPI();
+
+ async function doAbort() {
+ try {
+ await abortWithdrawal(withdrawUri.withdrawalOperationId);
+ onAborted();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
return (
- <section id="main" class="content">
- <h1 class="nav">{i18n.str`Charge your GNU Taler wallet`}</h1>
- <article>
- <div class="qr-div ">
- <a href={talerWithdrawUri} class="pure-button pure-button-primary">
- <i18n.Translate>Continue with GNU Taler</i18n.Translate>
- </a>
- <p>{i18n.str`Or scan this QR code with your mobile to receive the coin in another device:`}</p>
- <QR text={talerWithdrawUri} />
- <a
- class="pure-button btn-cancel"
- onClick={async (e) => {
- e.preventDefault();
- try {
- await abortWithdrawal(withdrawUri.withdrawalOperationId);
- onAborted();
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Conflict
- ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- >{i18n.str`Cancel`}</a>
+ <Fragment>
+ <div class="bg-white shadow-xl sm:rounded-lg">
+ <div class="px-4 py-5 sm:p-6">
+ <h3 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>If you have a Taler wallet installed in this device</i18n.Translate>
+ </h3>
+ <div class="mt-4 mb-4 text-sm text-gray-500">
+ <p><i18n.Translate>
+ You will see the details of the operation in your wallet including the fees (if applies).
+ If you still don't have one you can install it from <a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html">here</a>.
+ </i18n.Translate></p>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 pt-2 mt-2 ">
+ <button type="button"
+ // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md px-3 py-2 text-sm font-semibold text-black shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={doAbort}
+ >
+ Cancel
+ </button>
+ <a href={talerWithdrawUri}
+ class="inline-flex items-center 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"
+ >
+ <i18n.Translate>Withdraw</i18n.Translate>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="bg-white shadow-xl sm:rounded-lg mt-8">
+ <div class="px-4 py-5 sm:p-6">
+ <h3 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Or if you have the wallet in another device</i18n.Translate>
+ </h3>
+ <div class="mt-4 max-w-xl text-sm text-gray-500">
+ <i18n.Translate>Scan the QR below to start the withdrawal</i18n.Translate>
+ </div>
+ <div class="mt-2 max-w-md ml-auto mr-auto">
+ <QR text={talerWithdrawUri} />
+ </div>
</div>
- </article>
- </section>
+ <div class="flex items-center justify-center gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <button type="button"
+ // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md px-3 py-2 text-sm font-semibold text-black shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={doAbort}
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+
+ </Fragment>
);
}
+
+
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index ded48564f..9ac93bb34 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -13,26 +13,31 @@
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 { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
+import { HttpStatusCode, Logger, TranslatedString } from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { useTestingAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { bankUiSettings } from "../settings.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
+import { getRandomPassword, getRandomUsername } from "./rnd.js";
+import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
const logger = new Logger("RegistrationPage");
export function RegistrationPage({
onComplete,
+ onCancel
}: {
onComplete: () => void;
+ onCancel: () => void;
}): VNode {
const { i18n } = useTranslationContext();
if (!bankUiSettings.allowRegistrations) {
@@ -40,168 +45,357 @@ export function RegistrationPage({
<p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
);
}
- return <RegistrationForm onComplete={onComplete} />;
+ return <RegistrationForm onComplete={onComplete} onCancel={onCancel} />;
}
-export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
+export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/;
+export const PHONE_REGEX = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/;
+export const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
/**
* Collect and submit registration data.
*/
-function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
+function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, onCancel: () => void }): VNode {
const backend = useBackendContext();
const [username, setUsername] = useState<string | undefined>();
+ const [name, setName] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
+ const [phone, setPhone] = useState<string | undefined>();
+ const [email, setEmail] = useState<string | undefined>();
const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
+ const { requestNewLoginToken } = useCredentialsChecker()
const { register } = useTestingAPI();
const { i18n } = useTranslationContext();
const errors = undefinedIfEmpty({
+ // name: !name
+ // ? i18n.str`Missing name`
+ // : undefined,
username: !username
? i18n.str`Missing username`
: !USERNAME_REGEX.test(username)
- ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
- : undefined,
+ ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ : undefined,
+ phone: !phone
+ ? undefined
+ : !PHONE_REGEX.test(phone)
+ ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ : undefined,
+ email: !email
+ ? undefined
+ : !EMAIL_REGEX.test(email)
+ ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ : undefined,
password: !password ? i18n.str`Missing password` : undefined,
repeatPassword: !repeatPassword
? i18n.str`Missing password`
: repeatPassword !== password
- ? i18n.str`Passwords don't match`
- : undefined,
+ ? i18n.str`Passwords don't match`
+ : undefined,
});
+ async function doRegistrationStep() {
+ if (!username || !password) return;
+ try {
+ await register({ name: name ?? "", username, password });
+ const resp = await requestNewLoginToken(username, password)
+ setUsername(undefined);
+ if (resp.valid) {
+ backend.logIn({ username, token: resp.token });
+ }
+ onComplete();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`That username is already taken`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setPassword(undefined);
+ setRepeatPassword(undefined);
+ }
+
+ async function delay(ms: number): Promise<void> {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(undefined);
+ }, ms)
+ })
+ }
+ async function doRandomRegistration(tries: number = 3) {
+ const user = getRandomUsername();
+ const pass = getRandomPassword();
+ try {
+ setUsername(undefined);
+ setPassword(undefined);
+ setRepeatPassword(undefined);
+ const username = `_${user.first}-${user.second}_`
+ await register({ username, name: `${user.first} ${user.second}`, password: pass });
+ const resp = await requestNewLoginToken(username, pass)
+ if (resp.valid) {
+ backend.logIn({ username, token: resp.token });
+ }
+ onComplete();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ if (tries > 0) {
+ await delay(200)
+ await doRandomRegistration(tries - 1)
+ } else {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`Could not create a random user`
+ : undefined,
+ }),
+ );
+ }
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
return (
<Fragment>
- <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
- <article>
- <div class="register-div">
- <form
- class="register-form"
- noValidate
+ <h1 class="nav"></h1>
+
+ <div class="flex min-h-full flex-col justify-center">
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Account registration`}</h2>
+ </div>
+
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
+ <form class="space-y-6" noValidate
onSubmit={(e) => {
e.preventDefault();
}}
autoCapitalize="none"
autoCorrect="off"
>
- <div class="pure-form">
- <h2>{i18n.str`Please register!`}</h2>
- <p class="unameFieldLabel registerFieldLabel formFieldLabel">
- <label for="register-un">{i18n.str`Username:`}</label>
- </p>
- <input
- id="register-un"
- name="register-un"
- type="text"
- placeholder="Username"
- autocomplete="username"
- value={username ?? ""}
- onInput={(e): void => {
- setUsername(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={username !== undefined}
- />
- <p class="unameFieldLabel registerFieldLabel formFieldLabel">
- <label for="register-pw">{i18n.str`Password:`}</label>
- </p>
- <input
- type="password"
- name="register-pw"
- id="register-pw"
- placeholder="Password"
- autocomplete="new-password"
- value={password ?? ""}
- required
- onInput={(e): void => {
- setPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- <p class="unameFieldLabel registerFieldLabel formFieldLabel">
- <label for="register-repeat">{i18n.str`Repeat Password:`}</label>
- </p>
- <input
- type="password"
- style={{ marginBottom: 8 }}
- name="register-repeat"
- id="register-repeat"
- autocomplete="new-password"
- placeholder="Same password"
- value={repeatPassword ?? ""}
- required
- onInput={(e): void => {
- setRepeatPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.repeatPassword}
- isDirty={repeatPassword !== undefined}
- />
- <br />
- <button
- class="pure-button pure-button-primary btn-register"
- type="submit"
- disabled={!!errors}
- onClick={async (e) => {
- e.preventDefault();
-
- if (!username || !password) return;
- try {
- const credentials = { username, password };
- await register(credentials);
- setUsername(undefined);
- setPassword(undefined);
- setRepeatPassword(undefined);
- backend.logIn(credentials);
- onComplete();
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Conflict
- ? i18n.str`That username is already taken`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
+ <div>
+ <label for="username" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Username</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="username"
+ id="username"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={username ?? ""}
+ enterkeyhint="next"
+ placeholder="identification"
+ autocomplete="username"
+ required
+ onInput={(e): void => {
+ setUsername(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.username}
+ isDirty={username !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label for="password" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Password</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ </div>
+ <div class="mt-2">
+ <input
+ type="password"
+ name="password"
+ id="password"
+ autocomplete="current-password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ enterkeyhint="send"
+ value={password ?? ""}
+ placeholder="Password"
+ required
+ onInput={(e): void => {
+ setPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <div class="flex items-center justify-between">
+ <label for="register-repeat" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Repeat password</i18n.Translate>
+ <b style={{ color: "red" }}> *</b>
+ </label>
+ </div>
+ <div class="mt-2">
+ <input
+ type="password"
+ name="register-repeat"
+ id="register-repeat"
+ autocomplete="current-password"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ enterkeyhint="send"
+ value={repeatPassword ?? ""}
+ placeholder="Same password"
+ required
+ onInput={(e): void => {
+ setRepeatPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.repeatPassword}
+ isDirty={repeatPassword !== undefined}
+ />
+ </div>
+ </div>
+
+ <div>
+ <label for="name" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Name</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="name"
+ id="name"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={name ?? ""}
+ enterkeyhint="next"
+ placeholder="your name"
+ autocomplete="name"
+ required
+ onInput={(e): void => {
+ setName(e.currentTarget.value);
+ }}
+ />
+ {/* <ShowInputErrorLabel
+ message={errors?.name}
+ isDirty={name !== undefined}
+ /> */}
+ </div>
+ </div>
+
+ {/* <div>
+ <label for="phone" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Phone</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="phone"
+ id="phone"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={phone ?? ""}
+ enterkeyhint="next"
+ placeholder="your phone"
+ autocomplete="none"
+ onInput={(e): void => {
+ setPhone(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.phone}
+ isDirty={phone !== undefined}
+ />
+ </div>
+ </div>
+ <div>
+ <label for="email" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Email</i18n.Translate>
+ </label>
+ <div class="mt-2">
+ <input
+ autoFocus
+ type="text"
+ name="email"
+ id="email"
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={email ?? ""}
+ enterkeyhint="next"
+ placeholder="your email"
+ autocomplete="email"
+ onInput={(e): void => {
+ setEmail(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.email}
+ isDirty={email !== undefined}
+ />
+ </div>
+ </div> */}
+
+ <div class="flex w-full justify-between">
+ <button type="submit"
+ class="ring-1 ring-gray-600 rounded-md bg-white disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-white-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
+ onClick={(e) => {
+ e.preventDefault()
+ onCancel()
}}
>
- {i18n.str`Register`}
+ <i18n.Translate>Cancel</i18n.Translate>
</button>
- {/* FIXME: should use a different color */}
- <button
- class="pure-button pure-button-secondary btn-cancel"
+ <button type="submit"
+ class=" rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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();
- setUsername(undefined);
- setPassword(undefined);
- setRepeatPassword(undefined);
- onComplete();
+ e.preventDefault()
+ doRegistrationStep()
}}
>
- {i18n.str`Cancel`}
+ <i18n.Translate>Register</i18n.Translate>
</button>
</div>
+
</form>
+
+ {bankUiSettings.allowRandomAccountCreation &&
+ <p class="mt-10 text-center text-sm text-gray-500 border-t">
+ <button type="submit"
+ class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
+ onClick={(e) => {
+ e.preventDefault()
+ doRandomRegistration()
+ }}
+ >
+ <i18n.Translate>Create a random user</i18n.Translate>
+ </button>
+ </p>
+ }
</div>
- </article>
+ </div>
+
</Fragment>
);
}
diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx
deleted file mode 100644
index f176c73db..000000000
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { createHashHistory } from "history";
-import { VNode, h } from "preact";
-import { Route, Router, route } from "preact-router";
-import { useEffect, useMemo, useState } from "preact/hooks";
-import { BankFrame } from "./BankFrame.js";
-import { BusinessAccount } from "./BusinessAccount.js";
-import { HomePage, WithdrawalOperationPage } from "./HomePage.js";
-import { PublicHistoriesPage } from "./PublicHistoriesPage.js";
-import { RegistrationPage } from "./RegistrationPage.js";
-
-export function Routing(): VNode {
- const history = createHashHistory();
-
- return (
- <BankFrame
- goToBusinessAccount={() => {
- route("/business");
- }}
- >
- <Router history={history}>
- <Route
- path="/operation/:wopid"
- component={({ wopid }: { wopid: string }) => (
- <WithdrawalOperationPage
- operationId={wopid}
- onContinue={() => {
- route("/account");
- }}
- onLoadNotOk={() => {
- route("/account");
- }}
- />
- )}
- />
- <Route
- path="/public-accounts"
- component={() => <PublicHistoriesPage />}
- />
- <Route
- path="/register"
- component={() => (
- <RegistrationPage
- onComplete={() => {
- route("/account");
- }}
- />
- )}
- />
- <Route
- path="/account"
- component={() => (
- <HomePage
- onPendingOperationFound={(wopid) => {
- route(`/operation/${wopid}`);
- }}
- onRegister={() => {
- route("/register");
- }}
- />
- )}
- />
- <Route
- path="/business"
- component={() => (
- <BusinessAccount
- onClose={() => {
- route("/account");
- }}
- onRegister={() => {
- route("/register");
- }}
- onLoadNotOk={() => {
- route("/account");
- }}
- />
- )}
- />
- <Route default component={Redirect} to="/account" />
- </Router>
- </BankFrame>
- );
-}
-
-function Redirect({ to }: { to: string }): VNode {
- useEffect(() => {
- route(to, true);
- }, []);
- return <div>being redirected to {to}</div>;
-}
-
-export function assertUnreachable(x: never): never {
- throw new Error("Didn't expect to get here");
-}
diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
new file mode 100644
index 000000000..6acf0361e
--- /dev/null
+++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
@@ -0,0 +1,167 @@
+import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+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";
+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>;
+ }
+ 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>
+
+ </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>
+ <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();
+ doUpdate()
+ }}
+ />
+ </div>
+ </div>
+ </div>
+ </p>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
new file mode 100644
index 000000000..46f4fe0ef
--- /dev/null
+++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
@@ -0,0 +1,177 @@
+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 { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+
+export function UpdateAccountPassword({
+ 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>();
+
+ if (!result.ok) {
+ if (result.loading || result.type === ErrorType.TIMEOUT) {
+ return onLoadNotOk(result);
+ }
+ 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
+ ref={focus ? doAutoFocus : undefined}
+ 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>
+ {/* <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
+ 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>
+
+
+
+ </div>
+ </div>
+ <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/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 4c4a38e57..da299b1c8 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -19,40 +19,49 @@ import {
Amounts,
HttpStatusCode,
Logger,
+ TranslatedString,
+ WithdrawUriResult,
parseWithdrawUri,
} from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { Ref, VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
+import { forwardRef } from "preact/compat";
import { useEffect, useRef, useState } from "preact/hooks";
import { useAccessAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { forwardRef } from "preact/compat";
+import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js";
+import { useSettings } from "../hooks/settings.js";
+import { OperationState } from "./OperationState/index.js";
+import { Attention } from "../components/Attention.js";
const logger = new Logger("WalletWithdrawForm");
-const RefAmount = forwardRef(Amount);
+const RefAmount = forwardRef(InputAmount);
-export function WalletWithdrawForm({
- focus,
- limit,
- onSuccess,
-}: {
+
+function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
limit: AmountJson;
focus?: boolean;
- onSuccess: (operationId: string) => void;
+ goToConfirmOperation: (operationId: string) => void;
+ onCancel: () => void;
}): VNode {
const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+
const { createWithdrawal } = useAccessAPI();
+ const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`);
- const [amountStr, setAmountStr] = useState<string | undefined>("5.00");
- const ref = useRef<HTMLInputElement>(null);
- useEffect(() => {
- if (focus) ref.current?.focus();
- }, [focus]);
+ if (!!settings.currentWithdrawalOperationId) {
+ return <Attention type="warning" title={i18n.str`There is an operation already`}>
+ <i18n.Translate>
+ To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a>
+ </i18n.Translate>
+ </Attention>
+ }
const trimmedAmountStr = amountStr?.trim();
@@ -65,142 +74,186 @@ export function WalletWithdrawForm({
trimmedAmountStr == null
? i18n.str`required`
: !parsedAmount
- ? i18n.str`invalid`
- : Amounts.cmp(limit, parsedAmount) === -1
- ? i18n.str`balance is not enough`
- : undefined,
+ ? i18n.str`invalid`
+ : Amounts.cmp(limit, parsedAmount) === -1
+ ? i18n.str`balance is not enough`
+ : undefined,
});
- return (
- <form
- id="reserve-form"
- class="pure-form"
- name="tform"
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <p>
- <label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label>
- &nbsp;
- <RefAmount
- currency={limit.currency}
- value={amountStr}
- onChange={(v) => {
- setAmountStr(v);
- }}
- error={errors?.amount}
- ref={ref}
- />
- </p>
- <p>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary"
- type="submit"
- disabled={!!errors}
- value={i18n.str`Withdraw`}
- onClick={async (e) => {
- e.preventDefault();
- if (!parsedAmount) return;
- try {
- const result = await createWithdrawal({
- amount: Amounts.stringify(parsedAmount),
- });
- const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
- if (!uri) {
- return notifyError({
- title: i18n.str`Server responded with an invalid withdraw URI`,
- description: i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`,
- });
- } else {
- onSuccess(uri.withdrawalOperationId);
- }
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Forbidden
- ? i18n.str`The operation was rejected due to insufficient funds`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
+ async function doStart() {
+ if (!parsedAmount) return;
+ try {
+ const result = await createWithdrawal({
+ amount: Amounts.stringify(parsedAmount),
+ });
+ const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
+ if (!uri) {
+ return notifyError(
+ i18n.str`Server responded with an invalid withdraw URI`,
+ i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
+ } else {
+ updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
+ goToConfirmOperation(uri.withdrawalOperationId);
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The operation was rejected due to insufficient funds`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
+ return <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mt-4"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={e => {
+ e.preventDefault()
+ }}
+ >
+ <div class="px-4 py-6 ">
+ <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label for="withdraw-amount">{i18n.str`Amount`}</label>
+ <RefAmount
+ currency={limit.currency}
+ value={amountStr}
+ name="withdraw-amount"
+ onChange={(v) => {
+ setAmountStr(v);
}}
+ error={errors?.amount}
+ ref={focus ? doAutoFocus : undefined}
/>
</div>
- </p>
- </form>
- );
+ </div>
+ <div class="mt-4">
+ <div class="sm:inline">
+
+ <button type="button"
+ class=" inline-flex px-6 py-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("50.00")
+ }}
+ >
+ 50.00
+ </button>
+ <button type="button"
+ class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-r-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("25.00")
+ }}
+ >
+
+ 25.00
+ </button>
+ </div>
+ <div class="mt-4 sm:inline">
+ <button type="button"
+ class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-l-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("10.00")
+ }}
+ >
+ 10.00
+ </button>
+ <button type="button"
+ class=" inline-flex px-6 py-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("5.00")
+ }}
+ >
+ 5.00
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate></button>
+ <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={isRawPayto ? !!errorsPayto : !!errorsWire}
+ onClick={(e) => {
+ e.preventDefault()
+ doStart()
+ }}
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </button>
+ </div>
+
+ </form>
}
-export function Amount(
- {
- currency,
- value,
- error,
- onChange,
- }: {
- error?: string;
- currency: string;
- value: string | undefined;
- onChange?: (s: string) => void;
- },
- ref: Ref<HTMLInputElement>,
-): VNode {
- return (
- <div style={{ width: "max-content" }}>
- <div>
- <input
- type="text"
- readonly
- class="currency-indicator"
- size={currency.length}
- maxLength={currency.length}
- tabIndex={-1}
- style={{
- borderTopRightRadius: 0,
- borderBottomRightRadius: 0,
- borderRight: 0,
- }}
- value={currency}
+
+export function WalletWithdrawForm({
+ focus,
+ limit,
+ onCancel,
+ goToConfirmOperation,
+}: {
+ limit: AmountJson;
+ focus?: boolean;
+ goToConfirmOperation: (operationId: string) => void;
+ onCancel: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+
+ 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>Prepare your wallet</i18n.Translate></h2>
+ <p class="mt-1 text-sm text-gray-500">
+ <i18n.Translate>After using your wallet you will need to confirm or cancel the operation on this site.</i18n.Translate>
+ </p>
+ </div>
+
+ <div class="col-span-2">
+ {settings.showInstallWallet &&
+ <Attention title={i18n.str`You need a GNU Taler Wallet`} onClose={() => {
+ updateSettings("showInstallWallet", false);
+ }}>
+ <i18n.Translate>
+ If you don't have one yet you can follow the instruction <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href="https://taler.net/en/wallet.html">here</a>
+ </i18n.Translate>
+ </Attention>
+ }
+
+ {!settings.fastWithdrawal ?
+ <OldWithdrawalForm
+ focus={focus}
+ limit={limit}
+ onCancel={onCancel}
+ goToConfirmOperation={goToConfirmOperation}
/>
- <input
- type="number"
- ref={ref}
- name="amount"
- id="amount"
- placeholder="0"
- style={{
- borderTopLeftRadius: 0,
- borderBottomLeftRadius: 0,
- borderLeft: 0,
- width: 150,
- color: "black",
- }}
- value={value ?? ""}
- disabled={!onChange}
- onInput={(e): void => {
- if (onChange) {
- onChange(e.currentTarget.value);
- }
- }}
+ :
+ <OperationState
+ currency={limit.currency}
+ onClose={onCancel}
/>
- </div>
- <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+ }
</div>
+ </div>
);
}
+
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index cdb612155..ddcd2492d 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -15,26 +15,41 @@
*/
import {
+ AmountJson,
+ Amounts,
HttpStatusCode,
Logger,
- WithdrawUriResult,
+ PaytoUri,
+ PaytoUriIBAN,
+ PaytoUriTalerBank,
+ TranslatedString,
+ WithdrawUriResult
} from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
+ notifyInfo,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useMemo, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAccessAnonAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { useSettings } from "../hooks/settings.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
const logger = new Logger("WithdrawalConfirmationQuestion");
interface Props {
onAborted: () => void;
withdrawUri: WithdrawUriResult;
+ details: {
+ account: PaytoUri,
+ reserve: string,
+ amount: AmountJson,
+ }
}
/**
* Additional authentication required to complete the operation.
@@ -42,9 +57,11 @@ interface Props {
*/
export function WithdrawalConfirmationQuestion({
onAborted,
+ details,
withdrawUri,
}: Props): VNode {
const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
const captchaNumbers = useMemo(() => {
return {
@@ -56,139 +73,263 @@ export function WithdrawalConfirmationQuestion({
const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI();
const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
const answer = parseInt(captchaAnswer ?? "", 10);
+ const [busy, setBusy] = useState<Record<string, undefined>>()
const errors = undefinedIfEmpty({
answer: !captchaAnswer
? i18n.str`Answer the question before continue`
: Number.isNaN(answer)
- ? i18n.str`The answer should be a number`
- : answer !== captchaNumbers.a + captchaNumbers.b
- ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
- : undefined,
- });
+ ? i18n.str`The answer should be a number`
+ : answer !== captchaNumbers.a + captchaNumbers.b
+ ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
+ : undefined,
+ }) ?? busy;
+
+ async function doTransfer() {
+ try {
+ setBusy({})
+ await confirmWithdrawal(
+ withdrawUri.withdrawalOperationId,
+ );
+ if (!settings.showWithdrawalSuccess) {
+ notifyInfo(i18n.str`Wire transfer completed!`)
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
+ : status === HttpStatusCode.UnprocessableEntity
+ ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+
+ async function doCancel() {
+ try {
+ setBusy({})
+ await abortWithdrawal(withdrawUri.withdrawalOperationId);
+ onAborted();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+
return (
<Fragment>
- <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
- <article>
- <div class="challenge-div">
- <form
- class="challenge-form"
- noValidate
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <div class="pure-form" id="captcha" name="capcha-form">
- <h2>{i18n.str`Authorize withdrawal by solving challenge`}</h2>
- <p>
- <label for="answer">
- {i18n.str`What is`}&nbsp;
- <em>
- {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
- </em>
- ?&nbsp;
- </label>
- &nbsp;
- <input
- name="answer"
- id="answer"
- value={captchaAnswer ?? ""}
- type="text"
- autoFocus
- required
- onInput={(e): void => {
- setCaptchaAnswer(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.answer}
- isDirty={captchaAnswer !== undefined}
- />
- </p>
- <p>
- <button
- type="submit"
- class="pure-button pure-button-primary btn-confirm"
- disabled={!!errors}
- onClick={async (e) => {
- e.preventDefault();
- try {
- await confirmWithdrawal(
- withdrawUri.withdrawalOperationId,
- );
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Conflict
- ? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
- : status === HttpStatusCode.UnprocessableEntity
- ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ <div class="bg-white shadow sm:rounded-lg">
+ <div class="px-4 py-5 sm:p-6">
+ <h3 class="text-base font-semibold text-gray-900">
+ <i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
+ </h3>
+ <div class="mt-2 max-w-xl text-sm text-gray-500">
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-3 sm:gap-x-3">
+
+ <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 ring-indigo-600"}>
+ <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" />
+ <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>challenge response test</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600" 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-gray-100 p-4 shadow-sm focus:outline-none 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" />
+ <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>using SMS</i18n.Translate>
+ </span>
+ <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>not available</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600 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-gray-100 p-4 shadow-sm focus:outline-none 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" />
+ <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>one time password</i18n.Translate>
+ </span>
+ <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>not available</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600 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>
+ </div>
+ <div class="mt-3 text-sm leading-6">
+
+ <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 text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer.</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">
+ <label for="withdraw-amount">{i18n.str`What is`}&nbsp;
+ <em>
+ {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
+ </em>
+ ?
+ </label>
+ <div class="mt-2">
+ <div class="relative rounded-md shadow-sm">
+ <input
+ type="text"
+ // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ aria-describedby="answer"
+ autoFocus
+ class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ value={captchaAnswer ?? ""}
+ required
+
+ name="answer"
+ id="answer"
+ autocomplete="off"
+ onChange={(e): void => {
+ setCaptchaAnswer(e.currentTarget.value)
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} />
+ </div>
+ </div>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={doCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate></button>
+ <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()
+ doTransfer()
+ }}
+ >
+ <i18n.Translate>Transfer</i18n.Translate>
+ </button>
+ </div>
+
+ </form>
+ </div>
+ </div>
+ <div class="px-4 mt-4 ">
+ <div class="w-full">
+ <div class="px-4 sm:px-0 text-sm">
+ <p><i18n.Translate>Wire transfer details</i18n.Translate></p>
+ </div>
+ <div class="mt-6 border-t border-gray-100">
+ <dl class="divide-y divide-gray-100">
+ {((): VNode => {
+ switch (details.account.targetType) {
+ case "iban": {
+ const p = details.account as PaytoUriIBAN
+ const name = p.params["receiver-name"]
+ return <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
+ </div>
+ {name &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
}
- }
- }}
- >
- {i18n.str`Confirm`}
- </button>
- &nbsp;
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={async (e) => {
- e.preventDefault();
- try {
- await abortWithdrawal(withdrawUri.withdrawalOperationId);
- onAborted();
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Conflict
- ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ case "x-taler-bank": {
+ const p = details.account as PaytoUriTalerBank
+ const name = p.params["receiver-name"]
+ return <Fragment>
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
+ </div>
+ {name &&
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
}
+ default:
+ return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
+ </div>
+
}
- }}
- >
- {i18n.str`Cancel`}
- </button>
- </p>
+ })()}
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount value={details.amount} />
+ </dd>
+ </div>
+ </dl>
+ </div>
</div>
- </form>
- <div class="hint">
- <p>
- <i18n.Translate>
- A this point, a <b>real</b> bank would ask for an additional
- authentication proof (PIN/TAN, one time password, ..), instead
- of a simple calculation.
- </i18n.Translate>
- </p>
+
</div>
</div>
- </article>
+ </div>
+
</Fragment>
);
}
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 80fdac3c8..91c5da718 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -15,15 +15,16 @@
*/
import {
+ Amounts,
HttpStatusCode,
Logger,
WithdrawUriResult,
+ parsePaytoUri
} from "@gnu-taler/taler-util";
-import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { Loading } from "../components/Loading.js";
import { useWithdrawalDetails } from "../hooks/access.js";
-import { notifyInfo } from "../hooks/notification.js";
import { useSettings } from "../hooks/settings.js";
import { handleNotOkResult } from "./HomePage.js";
import { QrCodeSection } from "./QrCodeSection.js";
@@ -33,8 +34,7 @@ const logger = new Logger("WithdrawalQRCode");
interface Props {
withdrawUri: WithdrawUriResult;
- onContinue: () => void;
- onLoadNotOk: () => void;
+ onClose: () => void;
}
/**
* Offer the QR code (and a clickable taler://-link) to
@@ -43,27 +43,15 @@ interface Props {
*/
export function WithdrawalQRCode({
withdrawUri,
- onContinue,
- onLoadNotOk,
+ onClose,
}: Props): VNode {
- const [settings, updateSettings] = useSettings();
- function clearCurrentWithdrawal(): void {
- updateSettings("currentWithdrawalOperationId", undefined);
- onContinue();
- }
const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
+
if (!result.ok) {
if (result.loading) {
return <Loading />;
}
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- ) {
- return <div>operation not found</div>;
- }
- onLoadNotOk();
return handleNotOkResult(i18n)(result);
}
const { data } = result;
@@ -84,12 +72,11 @@ export function WithdrawalQRCode({
</i18n.Translate>
</p>
<a class="pure-button pure-button-primary"
- style={{float:"right"}}
+ style={{ float: "right" }}
onClick={async (e) => {
e.preventDefault();
- clearCurrentWithdrawal()
- onContinue()
- }}>
+ onClose()
+ }}>
{i18n.str`Continue`}
</a>
@@ -98,57 +85,77 @@ export function WithdrawalQRCode({
}
if (data.confirmation_done) {
- return <section id="main" class="content">
- <h1 class="nav">{i18n.str`Operation completed`}</h1>
+ return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
+ <div>
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
+ <svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
+ </svg>
+ </div>
+ <div class="mt-3 text-center sm:mt-5">
+ <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
+ <i18n.Translate>Withdrawal confirmed</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="mt-5 sm:mt-6">
+ <button type="button"
+ class="inline-flex w-full justify-center 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={async (e) => {
+ e.preventDefault();
+ onClose()
+ }}>
+ <i18n.Translate>Done</i18n.Translate>
+ </button>
+ </div>
+ </div>
- <section id="assets" style={{maxWidth: 400, marginLeft: "auto", marginRight:"auto"}}>
- <p>
- <i18n.Translate>
- The wire transfer to the GNU Taler Exchange bank's account is completed, now the
- exchange will send the requested amount into your GNU Taler wallet.
- </i18n.Translate>
- </p>
- <p>
- <i18n.Translate>
- You can close this page now or continue to the account page.
- </i18n.Translate>
- </p>
- <div style={{textAlign:"center"}}>
- <a class="pure-button pure-button-primary"
- onClick={async (e) => {
- e.preventDefault();
- clearCurrentWithdrawal()
- onContinue()
- }}>
- {i18n.str`Continue`}
- </a>
- </div>
- </section>
- </section>
}
-
if (!data.selection_done) {
return (
<QrCodeSection
withdrawUri={withdrawUri}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
- clearCurrentWithdrawal()
- onContinue()
- }}
+ onClose()
+ }}
/>
);
}
+ if (!data.selected_reserve_pub) {
+ return <div>
+ the exchange is selcted but no reserve pub
+ </div>
+ }
+
+ const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account)
+
+ if (!account) {
+ return <div>
+ the exchange is selcted but no account
+ </div>
+ }
return (
<WithdrawalConfirmationQuestion
withdrawUri={withdrawUri}
+ details={{
+ account,
+ reserve: data.selected_reserve_pub,
+ amount: Amounts.parseOrThrow(data.amount)
+ }}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
- clearCurrentWithdrawal()
- onContinue()
- }}
+ onClose()
+ }}
/>
);
-} \ 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
new file mode 100644
index 000000000..676fc43d0
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/Account.tsx
@@ -0,0 +1,38 @@
+import { Amounts } from "@gnu-taler/taler-util";
+import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js";
+import { handleNotOkResult } from "../HomePage.js";
+import { useAccountDetails } from "../../hooks/access.js";
+import { useBackendContext } from "../../context/backend.js";
+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 !== "loggedOut" ? r.state.username : "admin";
+ const result = useAccountDetails(account);
+
+ if (!result.ok) {
+ return handleNotOkResult(i18n)(result);
+ }
+ const { data } = result;
+
+ const balance = Amounts.parseOrThrow(data.balance.amount);
+ const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
+
+ const debitThreshold = Amounts.parseOrThrow(result.data.debit_threshold);
+ 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
new file mode 100644
index 000000000..ed8bf610d
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -0,0 +1,315 @@
+import { ComponentChildren, VNode, h } from "preact";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { buildPayto, parsePaytoUri } from "@gnu-taler/taler-util";
+import { doAutoFocus } from "../PaytoWireTransferForm.js";
+
+const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
+const EMAIL_REGEX =
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
+
+/**
+ * Create valid account object to update or create
+ * Take template as initial values for the form
+ * Purpose indicate if all field al read only (show), part of them (update)
+ * or none (create)
+ * @param param0
+ * @returns
+ */
+export function AccountForm({
+ 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();
+
+ 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`
+ : !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,
+ }),
+ // 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="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={focus ? doAutoFocus : undefined}
+ 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;
+ }
+ 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
new file mode 100644
index 000000000..a6899e679
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -0,0 +1,132 @@
+import { h, VNode } from "preact";
+import { useBusinessAccounts } from "../../hooks/circuit.js";
+import { handleNotOkResult } from "../HomePage.js";
+import { AccountAction } from "./Home.js";
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
+
+interface Props {
+ onAction: (type: AccountAction, account: string) => void;
+ account: string | undefined;
+ onCreateAccount: () => void;
+}
+
+export function AccountList({ account, onAction, onCreateAccount }: Props): VNode {
+ const result = useBusinessAccounts({ account });
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <div />;
+ if (!result.ok) {
+ return handleNotOkResult(i18n)(result);
+ }
+
+ const { customers } = result.data;
+ 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">
+ <a href="#" class="text-indigo-600 hover:text-indigo-900"
+ onClick={(e) => {
+ e.preventDefault();
+ onAction("show-details", item.username)
+ }}
+ >
+ {item.username}
+ </a>
+
+
+ </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`
+ ) : (
+ <span class="amount">
+ <RenderAmount value={balance} negative={balanceIsDebit} />
+ </span>
+ )}
+ </td>
+ <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)
+ }}
+ >
+ change password
+ </a>
+ <br />
+ <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
+ e.preventDefault();
+ onAction("show-cashout", item.username)
+ }}
+ >
+ cashouts
+ </a>
+ <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>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+} \ 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
new file mode 100644
index 000000000..2146fc6f0
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -0,0 +1,101 @@
+import { RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h, Fragment } from "preact";
+import { useAdminAccountAPI } from "../../hooks/circuit.js";
+import { useState } from "preact/hooks";
+import { buildRequestErrorMessage } from "../../utils.js";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { getRandomPassword } from "../rnd.js";
+import { AccountForm } from "./AccountForm.js";
+
+export function CreateNewAccount({
+ onCancel,
+ onCreateSuccess,
+}: {
+ onCancel: () => void;
+ onCreateSuccess: (password: string) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { createAccount } = useAdminAccountAPI();
+ const [submitAccount, setSubmitAccount] = useState<
+ SandboxBackend.Circuit.CircuitAccountData | undefined
+ >();
+
+ 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(),
+ };
+
+ 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
+ )
+ }
+ }
+ }
+
+ 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
new file mode 100644
index 000000000..d50ff14b4
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/Home.tsx
@@ -0,0 +1,148 @@
+import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Cashouts } from "../../components/Cashouts/index.js";
+import { ShowCashoutDetails } from "../business/Home.js";
+import { handleNotOkResult } from "../HomePage.js";
+import { ShowAccountDetails } from "../ShowAccountDetails.js";
+import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
+import { AdminAccount } from "./Account.js";
+import { AccountList } from "./AccountList.js";
+import { CreateNewAccount } from "./CreateNewAccount.js";
+import { RemoveAccount } from "./RemoveAccount.js";
+import { Transactions } from "../../components/Transactions/index.js";
+
+/**
+ * Query account information and show QR code if there is pending withdrawal
+ */
+interface Props {
+ onRegister: () => void;
+}
+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>()
+
+ const [createAccount, setCreateAccount] = useState(false);
+
+ const { i18n } = useTranslationContext();
+
+ if (action) {
+ switch (action.type) {
+ case "show-cashouts-details": return <ShowCashoutDetails
+ id={action.account}
+ onLoadNotOk={handleNotOkResult(i18n)}
+ onCancel={() => {
+ setAction(undefined);
+ }}
+ />
+ case "show-cashout": return (
+ <div>
+ <div>
+ <h1 class="nav welcome-text">
+ <i18n.Translate>Cashout for account {action.account}</i18n.Translate>
+ </h1>
+ </div>
+ <Cashouts
+ account={action.account}
+ onSelected={(id) => {
+ setAction({
+ type: "show-cashouts-details",
+ account: action.account
+ });
+ }}
+ />
+ <p>
+ <input
+ class="pure-button"
+ type="submit"
+ value={i18n.str`Close`}
+ onClick={async (e) => {
+ e.preventDefault();
+ setAction(undefined);
+ }}
+ />
+ </p>
+ </div>
+ )
+ case "update-password": return <UpdateAccountPassword
+ account={action.account}
+ onLoadNotOk={handleNotOkResult(i18n)}
+ onUpdateSuccess={() => {
+ notifyInfo(i18n.str`Password changed`);
+ setAction(undefined);
+ }}
+ onCancel={() => {
+ setAction(undefined);
+ }}
+ />
+ case "remove-account": return <RemoveAccount
+ account={action.account}
+ onLoadNotOk={handleNotOkResult(i18n)}
+ onUpdateSuccess={() => {
+ notifyInfo(i18n.str`Account removed`);
+ setAction(undefined);
+ }}
+ onCancel={() => {
+ setAction(undefined);
+ }}
+ />
+ case "show-details": return <ShowAccountDetails
+ account={action.account}
+ onLoadNotOk={handleNotOkResult(i18n)}
+ onChangePassword={() => {
+ setAction({
+ type: "update-password",
+ account: action.account,
+ })
+ }}
+ onUpdateSuccess={() => {
+ notifyInfo(i18n.str`Account updated`);
+ setAction(undefined);
+ }}
+ onClear={() => {
+ setAction(undefined);
+ }}
+ />
+ }
+ }
+
+ if (createAccount) {
+ return (
+ <CreateNewAccount
+ onCancel={() => setCreateAccount(false)}
+ onCreateSuccess={(password) => {
+ notifyInfo(
+ i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
+ );
+ setCreateAccount(false);
+ }}
+ />
+ );
+ }
+
+ return (
+ <Fragment>
+
+ <AccountList
+ onCreateAccount={() => {
+ setCreateAccount(true);
+ }}
+ account={undefined}
+ onAction={(type, account) => setAction({ account, type })}
+
+ />
+
+ <AdminAccount onRegister={onRegister} />
+
+ <Transactions account="admin"/>
+ </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
new file mode 100644
index 000000000..b323b0d01
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -0,0 +1,171 @@
+import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+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, undefinedIfEmpty } from "../../utils.js";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { Attention } from "../../components/Attention.js";
+import { doAutoFocus } from "../PaytoWireTransferForm.js";
+
+export function RemoveAccount({
+ 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);
+ }
+ if (result.status === HttpStatusCode.NotFound) {
+ return <div>account not found</div>;
+ }
+ return onLoadNotOk(result);
+ }
+ 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 <Attention type="warning" title={i18n.str`Can't delete the account`} onClose={onCancel}>
+ <i18n.Translate>The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.</i18n.Translate>
+ </Attention>
+ }
+
+ 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>
+ <Attention type="warning" title={i18n.str`You are going to remove the account`}>
+ <i18n.Translate>This step can't be undone.</i18n.Translate>
+ </Attention>
+
+ <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={focus ? doAutoFocus : undefined}
+ 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>
+ <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>
+ </div>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/business/Home.tsx
index d9aa8fa36..1a84effcd 100644
--- a/packages/demobank-ui/src/pages/BusinessAccount.tsx
+++ b/packages/demobank-ui/src/pages/business/Home.tsx
@@ -17,65 +17,63 @@ import {
AmountJson,
Amounts,
HttpStatusCode,
- TranslatedString,
+ TranslatedString
} from "@gnu-taler/taler-util";
import {
HttpResponse,
HttpResponsePaginated,
RequestError,
+ notify,
+ notifyError,
+ notifyInfo,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { StateUpdater, useEffect, useState } from "preact/hooks";
-import { Cashouts } from "../components/Cashouts/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { useAccountDetails } from "../hooks/access.js";
+import { useEffect, useState } from "preact/hooks";
+import { Cashouts } from "../../components/Cashouts/index.js";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { useAccountDetails } from "../../hooks/access.js";
import {
useCashoutDetails,
useCircuitAccountAPI,
useEstimator,
useRatiosAndFeeConfig,
-} from "../hooks/circuit.js";
+} from "../../hooks/circuit.js";
import {
TanChannel,
buildRequestErrorMessage,
undefinedIfEmpty,
-} from "../utils.js";
-import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
-import { ErrorBannerFloat } from "./BankFrame.js";
-import { LoginForm } from "./LoginForm.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { handleNotOkResult } from "./HomePage.js";
-import { ErrorMessage, notifyInfo } from "../hooks/notification.js";
-import { Amount } from "./WalletWithdrawForm.js";
+} from "../../utils.js";
+import { handleNotOkResult } from "../HomePage.js";
+import { InputAmount } from "../PaytoWireTransferForm.js";
+import { ShowAccountDetails } from "../ShowAccountDetails.js";
+import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
interface Props {
+ account: string,
onClose: () => void;
onRegister: () => void;
onLoadNotOk: () => void;
}
export function BusinessAccount({
onClose,
+ account,
onLoadNotOk,
onRegister,
}: Props): VNode {
const { i18n } = useTranslationContext();
- const backend = useBackendContext();
const [updatePassword, setUpdatePassword] = useState(false);
const [newCashout, setNewcashout] = useState(false);
const [showCashoutDetails, setShowCashoutDetails] = useState<
string | undefined
>();
- if (backend.state.status === "loggedOut") {
- return <LoginForm onRegister={onRegister} />;
- }
if (newCashout) {
return (
<CreateCashout
- account={backend.state.username}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ account={account}
+ onLoadNotOk={handleNotOkResult(i18n)}
onCancel={() => {
setNewcashout(false);
}}
@@ -93,7 +91,7 @@ export function BusinessAccount({
return (
<ShowCashoutDetails
id={showCashoutDetails}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ onLoadNotOk={handleNotOkResult(i18n)}
onCancel={() => {
setShowCashoutDetails(undefined);
}}
@@ -103,13 +101,13 @@ export function BusinessAccount({
if (updatePassword) {
return (
<UpdateAccountPassword
- account={backend.state.username}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ account={account}
+ onLoadNotOk={handleNotOkResult(i18n)}
onUpdateSuccess={() => {
notifyInfo(i18n.str`Password changed`);
setUpdatePassword(false);
}}
- onClear={() => {
+ onCancel={() => {
setUpdatePassword(false);
}}
/>
@@ -118,8 +116,8 @@ export function BusinessAccount({
return (
<div>
<ShowAccountDetails
- account={backend.state.username}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ account={account}
+ onLoadNotOk={handleNotOkResult(i18n)}
onUpdateSuccess={() => {
notifyInfo(i18n.str`Account updated`);
}}
@@ -132,7 +130,7 @@ export function BusinessAccount({
<div class="active">
<h3>{i18n.str`Latest cashouts`}</h3>
<Cashouts
- account={backend.state.username}
+ account={account}
onSelected={(id) => {
setShowCashoutDetails(id);
}}
@@ -201,13 +199,13 @@ function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse<
(result.data.name !== oldResult.name ||
result.data.version !== oldResult.version ||
result.data.ratios_and_fees.buy_at_ratio !==
- oldResult.ratios_and_fees.buy_at_ratio ||
+ oldResult.ratios_and_fees.buy_at_ratio ||
result.data.ratios_and_fees.buy_in_fee !==
- oldResult.ratios_and_fees.buy_in_fee ||
+ oldResult.ratios_and_fees.buy_in_fee ||
result.data.ratios_and_fees.sell_at_ratio !==
- oldResult.ratios_and_fees.sell_at_ratio ||
+ oldResult.ratios_and_fees.sell_at_ratio ||
result.data.ratios_and_fees.sell_out_fee !==
- oldResult.ratios_and_fees.sell_out_fee ||
+ oldResult.ratios_and_fees.sell_out_fee ||
result.data.fiat_currency !== oldResult.fiat_currency);
return {
@@ -225,7 +223,6 @@ function CreateCashout({
const { i18n } = useTranslationContext();
const ratiosResult = useRatiosAndFeeConfig();
const result = useAccountDetails(account);
- const [error, saveError] = useState<ErrorMessage | undefined>();
const {
estimateByCredit: calculateFromCredit,
estimateByDebit: calculateFromDebit,
@@ -238,9 +235,10 @@ function CreateCashout({
const config = ratiosResult.data;
const balance = Amounts.parseOrThrow(result.data.balance.amount);
- const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
- const zero = Amounts.zeroOfCurrency(balance.currency);
const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
+
+ const debitThreshold = Amounts.parseOrThrow(result.data.debit_threshold);
+ const zero = Amounts.zeroOfCurrency(balance.currency);
const limit = balanceIsDebit
? Amounts.sub(debitThreshold, balance).amount
: Amounts.add(balance, debitThreshold).amount;
@@ -251,15 +249,14 @@ function CreateCashout({
const sellFee = !config.ratios_and_fees.sell_out_fee
? zero
: Amounts.parseOrThrow(
- `${balance.currency}:${config.ratios_and_fees.sell_out_fee}`,
- );
+ `${balance.currency}:${config.ratios_and_fees.sell_out_fee}`,
+ );
const fiatCurrency = config.fiat_currency;
if (!sellRate || sellRate < 0) return <div>error rate</div>;
const amount = Amounts.parseOrThrow(
- `${!form.isDebit ? fiatCurrency : balance.currency}:${
- !form.amount ? "0" : form.amount
+ `${!form.isDebit ? fiatCurrency : balance.currency}:${!form.amount ? "0" : form.amount
}`,
);
@@ -268,32 +265,32 @@ function CreateCashout({
calculateFromDebit(amount, sellFee, sellRate)
.then((r) => {
setCalc(r);
- saveError(undefined);
})
.catch((error) => {
- saveError(
+ notify(
error instanceof RequestError
? buildRequestErrorMessage(i18n, error.cause)
: {
- title: i18n.str`Could not estimate the cashout`,
- description: error.message,
- },
+ type: "error",
+ title: i18n.str`Could not estimate the cashout`,
+ description: error.message as TranslatedString
+ },
);
});
} else {
calculateFromCredit(amount, sellFee, sellRate)
.then((r) => {
setCalc(r);
- saveError(undefined);
})
.catch((error) => {
- saveError(
+ notify(
error instanceof RequestError
? buildRequestErrorMessage(i18n, error.cause)
: {
- title: i18n.str`Could not estimate the cashout`,
- description: error.message,
- },
+ type: "error",
+ title: i18n.str`Could not estimate the cashout`,
+ description: error.message,
+ },
);
});
}
@@ -308,22 +305,19 @@ function CreateCashout({
amount: !form.amount
? i18n.str`required`
: !amount
- ? i18n.str`could not be parsed`
- : Amounts.cmp(limit, calc.debit) === -1
- ? i18n.str`balance is not enough`
- : Amounts.cmp(calc.beforeFee, sellFee) === -1
- ? i18n.str`the total amount to transfer does not cover the fees`
- : Amounts.isZero(calc.credit)
- ? i18n.str`the total transfer at destination will be zero`
- : undefined,
+ ? i18n.str`could not be parsed`
+ : Amounts.cmp(limit, calc.debit) === -1
+ ? i18n.str`balance is not enough`
+ : Amounts.cmp(calc.beforeFee, sellFee) === -1
+ ? i18n.str`the total amount to transfer does not cover the fees`
+ : Amounts.isZero(calc.credit)
+ ? i18n.str`the total transfer at destination will be zero`
+ : undefined,
channel: !form.channel ? i18n.str`required` : undefined,
});
return (
<div>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
<h1>New cashout</h1>
<form class="pure-form">
<fieldset>
@@ -341,13 +335,15 @@ function CreateCashout({
/>
</fieldset>
<fieldset>
- <label>
+ <label for="amount">
{form.isDebit
? i18n.str`Amount to send`
: i18n.str`Amount to receive`}
+
</label>
<div style={{ display: "flex" }}>
- <Amount
+ <InputAmount
+ name="amount"
currency={amount.currency}
value={form.amount}
onChange={(v) => {
@@ -362,7 +358,6 @@ function CreateCashout({
type="checkbox"
name="asd"
onChange={(e): void => {
- console.log("asdasd", form.isDebit);
form.isDebit = !form.isDebit;
updateForm(structuredClone(form));
}}
@@ -376,24 +371,27 @@ function CreateCashout({
<input value={sellRate} disabled />
</fieldset>
<fieldset>
- <label>{i18n.str`Balance now`}</label>
- <Amount
+ <label for="balance-now">{i18n.str`Balance now`}</label>
+ <InputAmount
+ name="banace-now"
currency={balance.currency}
value={Amounts.stringifyValue(balance)}
/>
</fieldset>
<fieldset>
- <label
+ <label for="total-cost"
style={{ fontWeight: "bold", color: "red" }}
>{i18n.str`Total cost`}</label>
- <Amount
+ <InputAmount
+ name="total-cost"
currency={balance.currency}
value={Amounts.stringifyValue(calc.debit)}
/>
</fieldset>
<fieldset>
- <label>{i18n.str`Balance after`}</label>
- <Amount
+ <label for="balance-after">{i18n.str`Balance after`}</label>
+ <InputAmount
+ name="balance-after"
currency={balance.currency}
value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""}
/>
@@ -401,16 +399,18 @@ function CreateCashout({
{Amounts.isZero(sellFee) ? undefined : (
<Fragment>
<fieldset>
- <label>{i18n.str`Amount after conversion`}</label>
- <Amount
+ <label for="amount-conversiojn">{i18n.str`Amount after conversion`}</label>
+ <InputAmount
+ name="amount-conversion"
currency={fiatCurrency}
value={Amounts.stringifyValue(calc.beforeFee)}
/>
</fieldset>
<fieldset>
- <label>{i18n.str`Cashout fee`}</label>
- <Amount
+ <label form="cashout-fee">{i18n.str`Cashout fee`}</label>
+ <InputAmount
+ name="cashout-fee"
currency={fiatCurrency}
value={Amounts.stringifyValue(sellFee)}
/>
@@ -418,10 +418,11 @@ function CreateCashout({
</Fragment>
)}
<fieldset>
- <label
+ <label for="total"
style={{ fontWeight: "bold", color: "green" }}
>{i18n.str`Total cashout transfer`}</label>
- <Amount
+ <InputAmount
+ name="total"
currency={fiatCurrency}
value={Amounts.stringifyValue(calc.credit)}
/>
@@ -511,18 +512,18 @@ function CreateCashout({
onComplete(res.data.uuid);
} catch (error) {
if (error instanceof RequestError) {
- saveError(
+ notify(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.BadRequest
? i18n.str`The exchange rate was incorrectly applied`
: status === HttpStatusCode.Forbidden
- ? i18n.str`A institutional user tried the operation`
- : status === HttpStatusCode.Conflict
- ? i18n.str`Need a contact data where to send the TAN`
- : status === HttpStatusCode.PreconditionFailed
- ? i18n.str`The account does not have sufficient funds`
- : undefined,
+ ? i18n.str`A institutional user tried the operation`
+ : status === HttpStatusCode.Conflict
+ ? i18n.str`Need a contact data where to send the TAN`
+ : status === HttpStatusCode.PreconditionFailed
+ ? i18n.str`The account does not have sufficient funds`
+ : undefined,
onServerError: (status) =>
status === HttpStatusCode.ServiceUnavailable
? i18n.str`The bank does not support the TAN channel for this operation`
@@ -530,13 +531,12 @@ function CreateCashout({
}),
);
} else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
}
}
}}
@@ -565,7 +565,6 @@ export function ShowCashoutDetails({
const result = useCashoutDetails(id);
const { abortCashout, confirmCashout } = useCircuitAccountAPI();
const [code, setCode] = useState<string | undefined>(undefined);
- const [error, saveError] = useState<ErrorMessage | undefined>();
if (!result.ok) return onLoadNotOk(result);
const errors = undefinedIfEmpty({
code: !code ? i18n.str`required` : undefined,
@@ -574,9 +573,6 @@ export function ShowCashoutDetails({
return (
<div>
<h1>Cashout details {id}</h1>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
<form class="pure-form">
<fieldset>
<label>
@@ -661,24 +657,23 @@ export function ShowCashoutDetails({
onCancel();
} catch (error) {
if (error instanceof RequestError) {
- saveError(
+ notify(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.NotFound
? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
: status === HttpStatusCode.PreconditionFailed
- ? i18n.str`Cashout was already confimed`
- : undefined,
+ ? i18n.str`Cashout was already confimed`
+ : undefined,
}),
);
} else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
}
}
}}
@@ -699,28 +694,27 @@ export function ShowCashoutDetails({
});
} catch (error) {
if (error instanceof RequestError) {
- saveError(
+ notify(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.NotFound
? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
: status === HttpStatusCode.PreconditionFailed
- ? i18n.str`Cashout was already confimed`
- : status === HttpStatusCode.Conflict
- ? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation`
- : status === HttpStatusCode.Forbidden
- ? i18n.str`Invalid code`
- : undefined,
+ ? i18n.str`Cashout was already confimed`
+ : status === HttpStatusCode.Conflict
+ ? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation`
+ : status === HttpStatusCode.Forbidden
+ ? i18n.str`Invalid code`
+ : undefined,
}),
);
} else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
}
}
}}
diff --git a/packages/demobank-ui/src/pages/rnd.ts b/packages/demobank-ui/src/pages/rnd.ts
new file mode 100644
index 000000000..32c3a934f
--- /dev/null
+++ b/packages/demobank-ui/src/pages/rnd.ts
@@ -0,0 +1,2895 @@
+import { createEddsaKeyPair, encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"
+import { bankUiSettings } from "../settings.js"
+
+
+const noun = [
+ "people",
+ "history",
+ "way",
+ "art",
+ "world",
+ "information",
+ "map",
+ "two",
+ "family",
+ "government",
+ "health",
+ "system",
+ "computer",
+ "meat",
+ "year",
+ "thanks",
+ "music",
+ "person",
+ "reading",
+ "method",
+ "data",
+ "food",
+ "understanding",
+ "theory",
+ "law",
+ "bird",
+ "literature",
+ "problem",
+ "software",
+ "control",
+ "knowledge",
+ "power",
+ "ability",
+ "economics",
+ "love",
+ "internet",
+ "television",
+ "science",
+ "library",
+ "nature",
+ "fact",
+ "product",
+ "idea",
+ "temperature",
+ "investment",
+ "area",
+ "society",
+ "activity",
+ "story",
+ "industry",
+ "media",
+ "thing",
+ "oven",
+ "community",
+ "definition",
+ "safety",
+ "quality",
+ "development",
+ "language",
+ "management",
+ "player",
+ "variety",
+ "video",
+ "week",
+ "security",
+ "country",
+ "exam",
+ "movie",
+ "organization",
+ "equipment",
+ "physics",
+ "analysis",
+ "policy",
+ "series",
+ "thought",
+ "basis",
+ "boyfriend",
+ "direction",
+ "strategy",
+ "technology",
+ "army",
+ "camera",
+ "freedom",
+ "paper",
+ "environment",
+ "child",
+ "instance",
+ "month",
+ "truth",
+ "marketing",
+ "university",
+ "writing",
+ "article",
+ "department",
+ "difference",
+ "goal",
+ "news",
+ "audience",
+ "fishing",
+ "growth",
+ "income",
+ "marriage",
+ "user",
+ "combination",
+ "failure",
+ "meaning",
+ "medicine",
+ "philosophy",
+ "teacher",
+ "communication",
+ "night",
+ "chemistry",
+ "disease",
+ "disk",
+ "energy",
+ "nation",
+ "road",
+ "role",
+ "soup",
+ "advertising",
+ "location",
+ "success",
+ "addition",
+ "apartment",
+ "education",
+ "math",
+ "moment",
+ "painting",
+ "politics",
+ "attention",
+ "decision",
+ "event",
+ "property",
+ "shopping",
+ "student",
+ "wood",
+ "competition",
+ "distribution",
+ "entertainment",
+ "office",
+ "population",
+ "president",
+ "unit",
+ "category",
+ "cigarette",
+ "context",
+ "introduction",
+ "opportunity",
+ "performance",
+ "driver",
+ "flight",
+ "length",
+ "magazine",
+ "newspaper",
+ "relationship",
+ "teaching",
+ "cell",
+ "dealer",
+ "finding",
+ "lake",
+ "member",
+ "message",
+ "phone",
+ "scene",
+ "appearance",
+ "association",
+ "concept",
+ "customer",
+ "death",
+ "discussion",
+ "housing",
+ "inflation",
+ "insurance",
+ "mood",
+ "woman",
+ "advice",
+ "blood",
+ "effort",
+ "expression",
+ "importance",
+ "opinion",
+ "payment",
+ "reality",
+ "responsibility",
+ "situation",
+ "skill",
+ "statement",
+ "wealth",
+ "application",
+ "city",
+ "county",
+ "depth",
+ "estate",
+ "foundation",
+ "grandmother",
+ "heart",
+ "perspective",
+ "photo",
+ "recipe",
+ "studio",
+ "topic",
+ "collection",
+ "depression",
+ "imagination",
+ "passion",
+ "percentage",
+ "resource",
+ "setting",
+ "ad",
+ "agency",
+ "college",
+ "connection",
+ "criticism",
+ "debt",
+ "description",
+ "memory",
+ "patience",
+ "secretary",
+ "solution",
+ "administration",
+ "aspect",
+ "attitude",
+ "director",
+ "personality",
+ "psychology",
+ "recommendation",
+ "response",
+ "selection",
+ "storage",
+ "version",
+ "alcohol",
+ "argument",
+ "complaint",
+ "contract",
+ "emphasis",
+ "highway",
+ "loss",
+ "membership",
+ "possession",
+ "preparation",
+ "steak",
+ "union",
+ "agreement",
+ "cancer",
+ "currency",
+ "employment",
+ "engineering",
+ "entry",
+ "interaction",
+ "mixture",
+ "preference",
+ "region",
+ "republic",
+ "tradition",
+ "virus",
+ "actor",
+ "classroom",
+ "delivery",
+ "device",
+ "difficulty",
+ "drama",
+ "election",
+ "engine",
+ "football",
+ "guidance",
+ "hotel",
+ "owner",
+ "priority",
+ "protection",
+ "suggestion",
+ "tension",
+ "variation",
+ "anxiety",
+ "atmosphere",
+ "awareness",
+ "bath",
+ "bread",
+ "candidate",
+ "climate",
+ "comparison",
+ "confusion",
+ "construction",
+ "elevator",
+ "emotion",
+ "employee",
+ "employer",
+ "guest",
+ "height",
+ "leadership",
+ "mall",
+ "manager",
+ "operation",
+ "recording",
+ "sample",
+ "transportation",
+ "charity",
+ "cousin",
+ "disaster",
+ "editor",
+ "efficiency",
+ "excitement",
+ "extent",
+ "feedback",
+ "guitar",
+ "homework",
+ "leader",
+ "mom",
+ "outcome",
+ "permission",
+ "presentation",
+ "promotion",
+ "reflection",
+ "refrigerator",
+ "resolution",
+ "revenue",
+ "session",
+ "singer",
+ "tennis",
+ "basket",
+ "bonus",
+ "cabinet",
+ "childhood",
+ "church",
+ "clothes",
+ "coffee",
+ "dinner",
+ "drawing",
+ "hair",
+ "hearing",
+ "initiative",
+ "judgment",
+ "lab",
+ "measurement",
+ "mode",
+ "mud",
+ "orange",
+ "poetry",
+ "police",
+ "possibility",
+ "procedure",
+ "queen",
+ "ratio",
+ "relation",
+ "restaurant",
+ "satisfaction",
+ "sector",
+ "signature",
+ "significance",
+ "song",
+ "tooth",
+ "town",
+ "vehicle",
+ "volume",
+ "wife",
+ "accident",
+ "airport",
+ "appointment",
+ "arrival",
+ "assumption",
+ "baseball",
+ "chapter",
+ "committee",
+ "conversation",
+ "database",
+ "enthusiasm",
+ "error",
+ "explanation",
+ "farmer",
+ "gate",
+ "girl",
+ "hall",
+ "historian",
+ "hospital",
+ "injury",
+ "instruction",
+ "maintenance",
+ "manufacturer",
+ "meal",
+ "perception",
+ "pie",
+ "poem",
+ "presence",
+ "proposal",
+ "reception",
+ "replacement",
+ "revolution",
+ "river",
+ "son",
+ "speech",
+ "tea",
+ "village",
+ "warning",
+ "winner",
+ "worker",
+ "writer",
+ "assistance",
+ "breath",
+ "buyer",
+ "chest",
+ "chocolate",
+ "conclusion",
+ "contribution",
+ "cookie",
+ "courage",
+ "dad",
+ "desk",
+ "drawer",
+ "establishment",
+ "examination",
+ "garbage",
+ "grocery",
+ "honey",
+ "impression",
+ "improvement",
+ "independence",
+ "insect",
+ "inspection",
+ "inspector",
+ "king",
+ "ladder",
+ "menu",
+ "penalty",
+ "piano",
+ "potato",
+ "profession",
+ "professor",
+ "quantity",
+ "reaction",
+ "requirement",
+ "salad",
+ "sister",
+ "supermarket",
+ "tongue",
+ "weakness",
+ "wedding",
+ "affair",
+ "ambition",
+ "analyst",
+ "apple",
+ "assignment",
+ "assistant",
+ "bathroom",
+ "bedroom",
+ "beer",
+ "birthday",
+ "celebration",
+ "championship",
+ "cheek",
+ "client",
+ "consequence",
+ "departure",
+ "diamond",
+ "dirt",
+ "ear",
+ "fortune",
+ "friendship",
+ "funeral",
+ "gene",
+ "girlfriend",
+ "hat",
+ "indication",
+ "intention",
+ "lady",
+ "midnight",
+ "negotiation",
+ "obligation",
+ "passenger",
+ "pizza",
+ "platform",
+ "poet",
+ "pollution",
+ "recognition",
+ "reputation",
+ "shirt",
+ "sir",
+ "speaker",
+ "stranger",
+ "surgery",
+ "sympathy",
+ "tale",
+ "throat",
+ "trainer",
+ "uncle",
+ "youth",
+ "time",
+ "work",
+ "film",
+ "water",
+ "money",
+ "example",
+ "while",
+ "business",
+ "study",
+ "game",
+ "life",
+ "form",
+ "air",
+ "day",
+ "place",
+ "number",
+ "part",
+ "field",
+ "fish",
+ "back",
+ "process",
+ "heat",
+ "hand",
+ "experience",
+ "job",
+ "book",
+ "end",
+ "point",
+ "type",
+ "home",
+ "economy",
+ "value",
+ "body",
+ "market",
+ "guide",
+ "interest",
+ "state",
+ "radio",
+ "course",
+ "company",
+ "price",
+ "size",
+ "card",
+ "list",
+ "mind",
+ "trade",
+ "line",
+ "care",
+ "group",
+ "risk",
+ "word",
+ "fat",
+ "force",
+ "key",
+ "light",
+ "training",
+ "name",
+ "school",
+ "top",
+ "amount",
+ "level",
+ "order",
+ "practice",
+ "research",
+ "sense",
+ "service",
+ "piece",
+ "web",
+ "boss",
+ "sport",
+ "fun",
+ "house",
+ "page",
+ "term",
+ "test",
+ "answer",
+ "sound",
+ "focus",
+ "matter",
+ "kind",
+ "soil",
+ "board",
+ "oil",
+ "picture",
+ "access",
+ "garden",
+ "range",
+ "rate",
+ "reason",
+ "future",
+ "site",
+ "demand",
+ "exercise",
+ "image",
+ "case",
+ "cause",
+ "coast",
+ "action",
+ "age",
+ "bad",
+ "boat",
+ "record",
+ "result",
+ "section",
+ "building",
+ "mouse",
+ "cash",
+ "class",
+ "nothing",
+ "period",
+ "plan",
+ "store",
+ "tax",
+ "side",
+ "subject",
+ "space",
+ "rule",
+ "stock",
+ "weather",
+ "chance",
+ "figure",
+ "man",
+ "model",
+ "source",
+ "beginning",
+ "earth",
+ "program",
+ "chicken",
+ "design",
+ "feature",
+ "head",
+ "material",
+ "purpose",
+ "question",
+ "rock",
+ "salt",
+ "act",
+ "birth",
+ "car",
+ "dog",
+ "object",
+ "scale",
+ "sun",
+ "note",
+ "profit",
+ "rent",
+ "speed",
+ "style",
+ "war",
+ "bank",
+ "craft",
+ "half",
+ "inside",
+ "outside",
+ "standard",
+ "bus",
+ "exchange",
+ "eye",
+ "fire",
+ "position",
+ "pressure",
+ "stress",
+ "advantage",
+ "benefit",
+ "box",
+ "frame",
+ "issue",
+ "step",
+ "cycle",
+ "face",
+ "item",
+ "metal",
+ "paint",
+ "review",
+ "room",
+ "screen",
+ "structure",
+ "view",
+ "account",
+ "ball",
+ "discipline",
+ "medium",
+ "share",
+ "balance",
+ "bit",
+ "black",
+ "bottom",
+ "choice",
+ "gift",
+ "impact",
+ "machine",
+ "shape",
+ "tool",
+ "wind",
+ "address",
+ "average",
+ "career",
+ "culture",
+ "morning",
+ "pot",
+ "sign",
+ "table",
+ "task",
+ "condition",
+ "contact",
+ "credit",
+ "egg",
+ "hope",
+ "ice",
+ "network",
+ "north",
+ "square",
+ "attempt",
+ "date",
+ "effect",
+ "link",
+ "post",
+ "star",
+ "voice",
+ "capital",
+ "challenge",
+ "friend",
+ "self",
+ "shot",
+ "brush",
+ "couple",
+ "debate",
+ "exit",
+ "front",
+ "function",
+ "lack",
+ "living",
+ "plant",
+ "plastic",
+ "spot",
+ "summer",
+ "taste",
+ "theme",
+ "track",
+ "wing",
+ "brain",
+ "button",
+ "click",
+ "desire",
+ "foot",
+ "gas",
+ "influence",
+ "notice",
+ "rain",
+ "wall",
+ "base",
+ "damage",
+ "distance",
+ "feeling",
+ "pair",
+ "savings",
+ "staff",
+ "sugar",
+ "target",
+ "text",
+ "animal",
+ "author",
+ "budget",
+ "discount",
+ "file",
+ "ground",
+ "lesson",
+ "minute",
+ "officer",
+ "phase",
+ "reference",
+ "register",
+ "sky",
+ "stage",
+ "stick",
+ "title",
+ "trouble",
+ "bowl",
+ "bridge",
+ "campaign",
+ "character",
+ "club",
+ "edge",
+ "evidence",
+ "fan",
+ "letter",
+ "lock",
+ "maximum",
+ "novel",
+ "option",
+ "pack",
+ "park",
+ "plenty",
+ "quarter",
+ "skin",
+ "sort",
+ "weight",
+ "baby",
+ "background",
+ "carry",
+ "dish",
+ "factor",
+ "fruit",
+ "glass",
+ "joint",
+ "master",
+ "muscle",
+ "red",
+ "strength",
+ "traffic",
+ "trip",
+ "vegetable",
+ "appeal",
+ "chart",
+ "gear",
+ "ideal",
+ "kitchen",
+ "land",
+ "log",
+ "mother",
+ "net",
+ "party",
+ "principle",
+ "relative",
+ "sale",
+ "season",
+ "signal",
+ "spirit",
+ "street",
+ "tree",
+ "wave",
+ "belt",
+ "bench",
+ "commission",
+ "copy",
+ "drop",
+ "minimum",
+ "path",
+ "progress",
+ "project",
+ "sea",
+ "south",
+ "status",
+ "stuff",
+ "ticket",
+ "tour",
+ "angle",
+ "blue",
+ "breakfast",
+ "confidence",
+ "daughter",
+ "degree",
+ "doctor",
+ "dot",
+ "dream",
+ "duty",
+ "essay",
+ "father",
+ "fee",
+ "finance",
+ "hour",
+ "juice",
+ "limit",
+ "luck",
+ "milk",
+ "mouth",
+ "peace",
+ "pipe",
+ "seat",
+ "stable",
+ "storm",
+ "substance",
+ "team",
+ "trick",
+ "afternoon",
+ "bat",
+ "beach",
+ "blank",
+ "catch",
+ "chain",
+ "consideration",
+ "cream",
+ "crew",
+ "detail",
+ "gold",
+ "interview",
+ "kid",
+ "mark",
+ "match",
+ "mission",
+ "pain",
+ "pleasure",
+ "score",
+ "screw",
+ "sex",
+ "shop",
+ "shower",
+ "suit",
+ "tone",
+ "window",
+ "agent",
+ "band",
+ "block",
+ "bone",
+ "calendar",
+ "cap",
+ "coat",
+ "contest",
+ "corner",
+ "court",
+ "cup",
+ "district",
+ "door",
+ "east",
+ "finger",
+ "garage",
+ "guarantee",
+ "hole",
+ "hook",
+ "implement",
+ "layer",
+ "lecture",
+ "lie",
+ "manner",
+ "meeting",
+ "nose",
+ "parking",
+ "partner",
+ "profile",
+ "respect",
+ "rice",
+ "routine",
+ "schedule",
+ "swimming",
+ "telephone",
+ "tip",
+ "winter",
+ "airline",
+ "bag",
+ "battle",
+ "bed",
+ "bill",
+ "bother",
+ "cake",
+ "code",
+ "curve",
+ "designer",
+ "dimension",
+ "dress",
+ "ease",
+ "emergency",
+ "evening",
+ "extension",
+ "farm",
+ "fight",
+ "gap",
+ "grade",
+ "holiday",
+ "horror",
+ "horse",
+ "host",
+ "husband",
+ "loan",
+ "mistake",
+ "mountain",
+ "nail",
+ "noise",
+ "occasion",
+ "package",
+ "patient",
+ "pause",
+ "phrase",
+ "proof",
+ "race",
+ "relief",
+ "sand",
+ "sentence",
+ "shoulder",
+ "smoke",
+ "stomach",
+ "string",
+ "tourist",
+ "towel",
+ "vacation",
+ "west",
+ "wheel",
+ "wine",
+ "arm",
+ "aside",
+ "associate",
+ "bet",
+ "blow",
+ "border",
+ "branch",
+ "breast",
+ "brother",
+ "buddy",
+ "bunch",
+ "chip",
+ "coach",
+ "cross",
+ "document",
+ "draft",
+ "dust",
+ "expert",
+ "floor",
+ "god",
+ "golf",
+ "habit",
+ "iron",
+ "judge",
+ "knife",
+ "landscape",
+ "league",
+ "mail",
+ "mess",
+ "native",
+ "opening",
+ "parent",
+ "pattern",
+ "pin",
+ "pool",
+ "pound",
+ "request",
+ "salary",
+ "shame",
+ "shelter",
+ "shoe",
+ "silver",
+ "tackle",
+ "tank",
+ "trust",
+ "assist",
+ "bake",
+ "bar",
+ "bell",
+ "bike",
+ "blame",
+ "boy",
+ "brick",
+ "chair",
+ "closet",
+ "clue",
+ "collar",
+ "comment",
+ "conference",
+ "devil",
+ "diet",
+ "fear",
+ "fuel",
+ "glove",
+ "jacket",
+ "lunch",
+ "monitor",
+ "mortgage",
+ "nurse",
+ "pace",
+ "panic",
+ "peak",
+ "plane",
+ "reward",
+ "row",
+ "sandwich",
+ "shock",
+ "spite",
+ "spray",
+ "surprise",
+ "till",
+ "transition",
+ "weekend",
+ "welcome",
+ "yard",
+ "alarm",
+ "bend",
+ "bicycle",
+ "bite",
+ "blind",
+ "bottle",
+ "cable",
+ "candle",
+ "clerk",
+ "cloud",
+ "concert",
+ "counter",
+ "flower",
+ "grandfather",
+ "harm",
+ "knee",
+ "lawyer",
+ "leather",
+ "load",
+ "mirror",
+ "neck",
+ "pension",
+ "plate",
+ "purple",
+ "ruin",
+ "ship",
+ "skirt",
+ "slice",
+ "snow",
+ "specialist",
+ "stroke",
+ "switch",
+ "trash",
+ "tune",
+ "zone",
+ "anger",
+ "award",
+ "bid",
+ "bitter",
+ "boot",
+ "bug",
+ "camp",
+ "candy",
+ "carpet",
+ "cat",
+ "champion",
+ "channel",
+ "clock",
+ "comfort",
+ "cow",
+ "crack",
+ "engineer",
+ "entrance",
+ "fault",
+ "grass",
+ "guy",
+ "hell",
+ "highlight",
+ "incident",
+ "island",
+ "joke",
+ "jury",
+ "leg",
+ "lip",
+ "mate",
+ "motor",
+ "nerve",
+ "passage",
+ "pen",
+ "pride",
+ "priest",
+ "prize",
+ "promise",
+ "resident",
+ "resort",
+ "ring",
+ "roof",
+ "rope",
+ "sail",
+ "scheme",
+ "script",
+ "sock",
+ "station",
+ "toe",
+ "tower",
+ "truck",
+ "witness",
+ "a",
+ "you",
+ "it",
+ "can",
+ "will",
+ "if",
+ "one",
+ "many",
+ "most",
+ "other",
+ "use",
+ "make",
+ "good",
+ "look",
+ "help",
+ "go",
+ "great",
+ "being",
+ "few",
+ "might",
+ "still",
+ "public",
+ "read",
+ "keep",
+ "start",
+ "give",
+ "human",
+ "local",
+ "general",
+ "she",
+ "specific",
+ "long",
+ "play",
+ "feel",
+ "high",
+ "tonight",
+ "put",
+ "common",
+ "set",
+ "change",
+ "simple",
+ "past",
+ "big",
+ "possible",
+ "particular",
+ "today",
+ "major",
+ "personal",
+ "current",
+ "national",
+ "cut",
+ "natural",
+ "physical",
+ "show",
+ "try",
+ "check",
+ "second",
+ "call",
+ "move",
+ "pay",
+ "let",
+ "increase",
+ "single",
+ "individual",
+ "turn",
+ "ask",
+ "buy",
+ "guard",
+ "hold",
+ "main",
+ "offer",
+ "potential",
+ "professional",
+ "international",
+ "travel",
+ "cook",
+ "alternative",
+ "following",
+ "special",
+ "working",
+ "whole",
+ "dance",
+ "excuse",
+ "cold",
+ "commercial",
+ "low",
+ "purchase",
+ "deal",
+ "primary",
+ "worth",
+ "fall",
+ "necessary",
+ "positive",
+ "produce",
+ "search",
+ "present",
+ "spend",
+ "talk",
+ "creative",
+ "tell",
+ "cost",
+ "drive",
+ "green",
+ "support",
+ "glad",
+ "remove",
+ "return",
+ "run",
+ "complex",
+ "due",
+ "effective",
+ "middle",
+ "regular",
+ "reserve",
+ "independent",
+ "leave",
+ "original",
+ "reach",
+ "rest",
+ "serve",
+ "watch",
+ "beautiful",
+ "charge",
+ "active",
+ "break",
+ "negative",
+ "safe",
+ "stay",
+ "visit",
+ "visual",
+ "affect",
+ "cover",
+ "report",
+ "rise",
+ "walk",
+ "white",
+ "beyond",
+ "junior",
+ "pick",
+ "unique",
+ "anything",
+ "classic",
+ "final",
+ "lift",
+ "mix",
+ "private",
+ "stop",
+ "teach",
+ "western",
+ "concern",
+ "familiar",
+ "fly",
+ "official",
+ "broad",
+ "comfortable",
+ "gain",
+ "maybe",
+ "rich",
+ "save",
+ "stand",
+ "young",
+ "fail",
+ "heavy",
+ "hello",
+ "lead",
+ "listen",
+ "valuable",
+ "worry",
+ "handle",
+ "leading",
+ "meet",
+ "release",
+ "sell",
+ "finish",
+ "normal",
+ "press",
+ "ride",
+ "secret",
+ "spread",
+ "spring",
+ "tough",
+ "wait",
+ "brown",
+ "deep",
+ "display",
+ "flow",
+ "hit",
+ "objective",
+ "shoot",
+ "touch",
+ "cancel",
+ "chemical",
+ "cry",
+ "dump",
+ "extreme",
+ "push",
+ "conflict",
+ "eat",
+ "fill",
+ "formal",
+ "jump",
+ "kick",
+ "opposite",
+ "pass",
+ "pitch",
+ "remote",
+ "total",
+ "treat",
+ "vast",
+ "abuse",
+ "beat",
+ "burn",
+ "deposit",
+ "print",
+ "raise",
+ "sleep",
+ "somewhere",
+ "advance",
+ "anywhere",
+ "consist",
+ "dark",
+ "double",
+ "draw",
+ "equal",
+ "fix",
+ "hire",
+ "internal",
+ "join",
+ "kill",
+ "sensitive",
+ "tap",
+ "win",
+ "attack",
+ "claim",
+ "constant",
+ "drag",
+ "drink",
+ "guess",
+ "minor",
+ "pull",
+ "raw",
+ "soft",
+ "solid",
+ "wear",
+ "weird",
+ "wonder",
+ "annual",
+ "count",
+ "dead",
+ "doubt",
+ "feed",
+ "forever",
+ "impress",
+ "nobody",
+ "repeat",
+ "round",
+ "sing",
+ "slide",
+ "strip",
+ "whereas",
+ "wish",
+ "combine",
+ "command",
+ "dig",
+ "divide",
+ "equivalent",
+ "hang",
+ "hunt",
+ "initial",
+ "march",
+ "mention",
+ "smell",
+ "spiritual",
+ "survey",
+ "tie",
+ "adult",
+ "brief",
+ "crazy",
+ "escape",
+ "gather",
+ "hate",
+ "prior",
+ "repair",
+ "rough",
+ "sad",
+ "scratch",
+ "sick",
+ "strike",
+ "employ",
+ "external",
+ "hurt",
+ "illegal",
+ "laugh",
+ "lay",
+ "mobile",
+ "nasty",
+ "ordinary",
+ "respond",
+ "royal",
+ "senior",
+ "split",
+ "strain",
+ "struggle",
+ "swim",
+ "train",
+ "upper",
+ "wash",
+ "yellow",
+ "convert",
+ "crash",
+ "dependent",
+ "fold",
+ "funny",
+ "grab",
+ "hide",
+ "miss",
+ "permit",
+ "quote",
+ "recover",
+ "resolve",
+ "roll",
+ "sink",
+ "slip",
+ "spare",
+ "suspect",
+ "sweet",
+ "swing",
+ "twist",
+ "upstairs",
+ "usual",
+ "abroad",
+ "brave",
+ "calm",
+ "concentrate",
+ "estimate",
+ "grand",
+ "male",
+ "mine",
+ "prompt",
+ "quiet",
+ "refuse",
+ "regret",
+ "reveal",
+ "rush",
+ "shake",
+ "shift",
+ "shine",
+ "steal",
+ "suck",
+ "surround",
+ "anybody",
+ "bear",
+ "brilliant",
+ "dare",
+ "dear",
+ "delay",
+ "drunk",
+ "female",
+ "hurry",
+ "inevitable",
+ "invite",
+ "kiss",
+ "neat",
+ "pop",
+ "punch",
+ "quit",
+ "reply",
+ "representative",
+ "resist",
+ "rip",
+ "rub",
+ "silly",
+ "smile",
+ "spell",
+ "stretch",
+ "stupid",
+ "tear",
+ "temporary",
+ "tomorrow",
+ "wake",
+ "wrap",
+ "yesterday"
+]
+
+const adj = [
+ "abandoned",
+ "able",
+ "absolute",
+ "adorable",
+ "adventurous",
+ "academic",
+ "acceptable",
+ "acclaimed",
+ "accomplished",
+ "accurate",
+ "aching",
+ "acidic",
+ "acrobatic",
+ "active",
+ "actual",
+ "adept",
+ "admirable",
+ "admired",
+ "adolescent",
+ "adorable",
+ "adored",
+ "advanced",
+ "afraid",
+ "affectionate",
+ "aged",
+ "aggravating",
+ "aggressive",
+ "agile",
+ "agitated",
+ "agonizing",
+ "agreeable",
+ "ajar",
+ "alarmed",
+ "alarming",
+ "alert",
+ "alienated",
+ "alive",
+ "all",
+ "altruistic",
+ "amazing",
+ "ambitious",
+ "ample",
+ "amused",
+ "amusing",
+ "anchored",
+ "ancient",
+ "angelic",
+ "angry",
+ "anguished",
+ "animated",
+ "annual",
+ "another",
+ "antique",
+ "anxious",
+ "any",
+ "apprehensive",
+ "appropriate",
+ "apt",
+ "arctic",
+ "arid",
+ "aromatic",
+ "artistic",
+ "ashamed",
+ "assured",
+ "astonishing",
+ "athletic",
+ "attached",
+ "attentive",
+ "attractive",
+ "austere",
+ "authentic",
+ "authorized",
+ "automatic",
+ "avaricious",
+ "average",
+ "aware",
+ "awesome",
+ "awful",
+ "awkward",
+ "babyish",
+ "bad",
+ "back",
+ "baggy",
+ "bare",
+ "barren",
+ "basic",
+ "beautiful",
+ "belated",
+ "beloved",
+ "beneficial",
+ "better",
+ "best",
+ "bewitched",
+ "big",
+ "big-hearted",
+ "biodegradable",
+ "bite-sized",
+ "bitter",
+ "black",
+ "black-and-white",
+ "bland",
+ "blank",
+ "blaring",
+ "bleak",
+ "blind",
+ "blissful",
+ "blond",
+ "blue",
+ "blushing",
+ "bogus",
+ "boiling",
+ "bold",
+ "bony",
+ "boring",
+ "bossy",
+ "both",
+ "bouncy",
+ "bountiful",
+ "bowed",
+ "brave",
+ "breakable",
+ "brief",
+ "bright",
+ "brilliant",
+ "brisk",
+ "broken",
+ "bronze",
+ "brown",
+ "bruised",
+ "bubbly",
+ "bulky",
+ "bumpy",
+ "buoyant",
+ "burdensome",
+ "burly",
+ "bustling",
+ "busy",
+ "buttery",
+ "buzzing",
+ "calculating",
+ "calm",
+ "candid",
+ "canine",
+ "capital",
+ "carefree",
+ "careful",
+ "careless",
+ "caring",
+ "cautious",
+ "cavernous",
+ "celebrated",
+ "charming",
+ "cheap",
+ "cheerful",
+ "cheery",
+ "chief",
+ "chilly",
+ "chubby",
+ "circular",
+ "classic",
+ "clean",
+ "clear",
+ "clear-cut",
+ "clever",
+ "close",
+ "closed",
+ "cloudy",
+ "clueless",
+ "clumsy",
+ "cluttered",
+ "coarse",
+ "cold",
+ "colorful",
+ "colorless",
+ "colossal",
+ "comfortable",
+ "common",
+ "compassionate",
+ "competent",
+ "complete",
+ "complex",
+ "complicated",
+ "composed",
+ "concerned",
+ "concrete",
+ "confused",
+ "conscious",
+ "considerate",
+ "constant",
+ "content",
+ "conventional",
+ "cooked",
+ "cool",
+ "cooperative",
+ "coordinated",
+ "corny",
+ "corrupt",
+ "costly",
+ "courageous",
+ "courteous",
+ "crafty",
+ "crazy",
+ "creamy",
+ "creative",
+ "creepy",
+ "criminal",
+ "crisp",
+ "critical",
+ "crooked",
+ "crowded",
+ "cruel",
+ "crushing",
+ "cuddly",
+ "cultivated",
+ "cultured",
+ "cumbersome",
+ "curly",
+ "curvy",
+ "cute",
+ "cylindrical",
+ "damaged",
+ "damp",
+ "dangerous",
+ "dapper",
+ "daring",
+ "darling",
+ "dark",
+ "dazzling",
+ "dead",
+ "deadly",
+ "deafening",
+ "dear",
+ "dearest",
+ "decent",
+ "decimal",
+ "decisive",
+ "deep",
+ "defenseless",
+ "defensive",
+ "defiant",
+ "deficient",
+ "definite",
+ "definitive",
+ "delayed",
+ "delectable",
+ "delicious",
+ "delightful",
+ "delirious",
+ "demanding",
+ "dense",
+ "dental",
+ "dependable",
+ "dependent",
+ "descriptive",
+ "deserted",
+ "detailed",
+ "determined",
+ "devoted",
+ "different",
+ "difficult",
+ "digital",
+ "diligent",
+ "dim",
+ "dimpled",
+ "dimwitted",
+ "direct",
+ "disastrous",
+ "discrete",
+ "disfigured",
+ "disgusting",
+ "disloyal",
+ "dismal",
+ "distant",
+ "downright",
+ "dreary",
+ "dirty",
+ "disguised",
+ "dishonest",
+ "dismal",
+ "distant",
+ "distinct",
+ "distorted",
+ "dizzy",
+ "dopey",
+ "doting",
+ "double",
+ "downright",
+ "drab",
+ "drafty",
+ "dramatic",
+ "dreary",
+ "droopy",
+ "dry",
+ "dual",
+ "dull",
+ "dutiful",
+ "each",
+ "eager",
+ "earnest",
+ "early",
+ "easy",
+ "easy-going",
+ "ecstatic",
+ "edible",
+ "educated",
+ "elaborate",
+ "elastic",
+ "elated",
+ "elderly",
+ "electric",
+ "elegant",
+ "elementary",
+ "elliptical",
+ "embarrassed",
+ "embellished",
+ "eminent",
+ "emotional",
+ "empty",
+ "enchanted",
+ "enchanting",
+ "energetic",
+ "enlightened",
+ "enormous",
+ "enraged",
+ "entire",
+ "envious",
+ "equal",
+ "equatorial",
+ "essential",
+ "esteemed",
+ "ethical",
+ "euphoric",
+ "even",
+ "evergreen",
+ "everlasting",
+ "every",
+ "evil",
+ "exalted",
+ "excellent",
+ "exemplary",
+ "exhausted",
+ "excitable",
+ "excited",
+ "exciting",
+ "exotic",
+ "expensive",
+ "experienced",
+ "expert",
+ "extraneous",
+ "extroverted",
+ "extra-large",
+ "extra-small",
+ "fabulous",
+ "failing",
+ "faint",
+ "fair",
+ "faithful",
+ "fake",
+ "false",
+ "familiar",
+ "famous",
+ "fancy",
+ "fantastic",
+ "far",
+ "faraway",
+ "far-flung",
+ "far-off",
+ "fast",
+ "fat",
+ "fatal",
+ "fatherly",
+ "favorable",
+ "favorite",
+ "fearful",
+ "fearless",
+ "feisty",
+ "feline",
+ "female",
+ "feminine",
+ "few",
+ "fickle",
+ "filthy",
+ "fine",
+ "finished",
+ "firm",
+ "first",
+ "firsthand",
+ "fitting",
+ "fixed",
+ "flaky",
+ "flamboyant",
+ "flashy",
+ "flat",
+ "flawed",
+ "flawless",
+ "flickering",
+ "flimsy",
+ "flippant",
+ "flowery",
+ "fluffy",
+ "fluid",
+ "flustered",
+ "focused",
+ "fond",
+ "foolhardy",
+ "foolish",
+ "forceful",
+ "forked",
+ "formal",
+ "forsaken",
+ "forthright",
+ "fortunate",
+ "fragrant",
+ "frail",
+ "frank",
+ "frayed",
+ "free",
+ "French",
+ "fresh",
+ "frequent",
+ "friendly",
+ "frightened",
+ "frightening",
+ "frigid",
+ "frilly",
+ "frizzy",
+ "frivolous",
+ "front",
+ "frosty",
+ "frozen",
+ "frugal",
+ "fruitful",
+ "full",
+ "fumbling",
+ "functional",
+ "funny",
+ "fussy",
+ "fuzzy",
+ "gargantuan",
+ "gaseous",
+ "general",
+ "generous",
+ "gentle",
+ "genuine",
+ "giant",
+ "giddy",
+ "gigantic",
+ "gifted",
+ "giving",
+ "glamorous",
+ "glaring",
+ "glass",
+ "gleaming",
+ "gleeful",
+ "glistening",
+ "glittering",
+ "gloomy",
+ "glorious",
+ "glossy",
+ "glum",
+ "golden",
+ "good",
+ "good-natured",
+ "gorgeous",
+ "graceful",
+ "gracious",
+ "grand",
+ "grandiose",
+ "granular",
+ "grateful",
+ "grave",
+ "gray",
+ "great",
+ "greedy",
+ "green",
+ "gregarious",
+ "grim",
+ "grimy",
+ "gripping",
+ "grizzled",
+ "gross",
+ "grotesque",
+ "grouchy",
+ "grounded",
+ "growing",
+ "growling",
+ "grown",
+ "grubby",
+ "gruesome",
+ "grumpy",
+ "guilty",
+ "gullible",
+ "gummy",
+ "hairy",
+ "half",
+ "handmade",
+ "handsome",
+ "handy",
+ "happy",
+ "happy-go-lucky",
+ "hard",
+ "hard-to-find",
+ "harmful",
+ "harmless",
+ "harmonious",
+ "harsh",
+ "hasty",
+ "hateful",
+ "haunting",
+ "healthy",
+ "heartfelt",
+ "hearty",
+ "heavenly",
+ "heavy",
+ "hefty",
+ "helpful",
+ "helpless",
+ "hidden",
+ "hideous",
+ "high",
+ "high-level",
+ "hilarious",
+ "hoarse",
+ "hollow",
+ "homely",
+ "honest",
+ "honorable",
+ "honored",
+ "hopeful",
+ "horrible",
+ "hospitable",
+ "hot",
+ "huge",
+ "humble",
+ "humiliating",
+ "humming",
+ "humongous",
+ "hungry",
+ "hurtful",
+ "husky",
+ "icky",
+ "icy",
+ "ideal",
+ "idealistic",
+ "identical",
+ "idle",
+ "idiotic",
+ "idolized",
+ "ignorant",
+ "ill",
+ "illegal",
+ "ill-fated",
+ "ill-informed",
+ "illiterate",
+ "illustrious",
+ "imaginary",
+ "imaginative",
+ "immaculate",
+ "immaterial",
+ "immediate",
+ "immense",
+ "impassioned",
+ "impeccable",
+ "impartial",
+ "imperfect",
+ "imperturbable",
+ "impish",
+ "impolite",
+ "important",
+ "impossible",
+ "impractical",
+ "impressionable",
+ "impressive",
+ "improbable",
+ "impure",
+ "inborn",
+ "incomparable",
+ "incompatible",
+ "incomplete",
+ "inconsequential",
+ "incredible",
+ "indelible",
+ "inexperienced",
+ "indolent",
+ "infamous",
+ "infantile",
+ "infatuated",
+ "inferior",
+ "infinite",
+ "informal",
+ "innocent",
+ "insecure",
+ "insidious",
+ "insignificant",
+ "insistent",
+ "instructive",
+ "insubstantial",
+ "intelligent",
+ "intent",
+ "intentional",
+ "interesting",
+ "internal",
+ "international",
+ "intrepid",
+ "ironclad",
+ "irresponsible",
+ "irritating",
+ "itchy",
+ "jaded",
+ "jagged",
+ "jam-packed",
+ "jaunty",
+ "jealous",
+ "jittery",
+ "joint",
+ "jolly",
+ "jovial",
+ "joyful",
+ "joyous",
+ "jubilant",
+ "judicious",
+ "juicy",
+ "jumbo",
+ "junior",
+ "jumpy",
+ "juvenile",
+ "kaleidoscopic",
+ "keen",
+ "key",
+ "kind",
+ "kindhearted",
+ "kindly",
+ "klutzy",
+ "knobby",
+ "knotty",
+ "knowledgeable",
+ "knowing",
+ "known",
+ "kooky",
+ "kosher",
+ "lame",
+ "lanky",
+ "large",
+ "last",
+ "lasting",
+ "late",
+ "lavish",
+ "lawful",
+ "lazy",
+ "leading",
+ "lean",
+ "leafy",
+ "left",
+ "legal",
+ "legitimate",
+ "light",
+ "lighthearted",
+ "likable",
+ "likely",
+ "limited",
+ "limp",
+ "limping",
+ "linear",
+ "lined",
+ "liquid",
+ "little",
+ "live",
+ "lively",
+ "livid",
+ "loathsome",
+ "lone",
+ "lonely",
+ "long",
+ "long-term",
+ "loose",
+ "lopsided",
+ "lost",
+ "loud",
+ "lovable",
+ "lovely",
+ "loving",
+ "low",
+ "loyal",
+ "lucky",
+ "lumbering",
+ "luminous",
+ "lumpy",
+ "lustrous",
+ "luxurious",
+ "mad",
+ "made-up",
+ "magnificent",
+ "majestic",
+ "major",
+ "male",
+ "mammoth",
+ "married",
+ "marvelous",
+ "masculine",
+ "massive",
+ "mature",
+ "meager",
+ "mealy",
+ "mean",
+ "measly",
+ "meaty",
+ "medical",
+ "mediocre",
+ "medium",
+ "meek",
+ "mellow",
+ "melodic",
+ "memorable",
+ "menacing",
+ "merry",
+ "messy",
+ "metallic",
+ "mild",
+ "milky",
+ "mindless",
+ "miniature",
+ "minor",
+ "minty",
+ "miserable",
+ "miserly",
+ "misguided",
+ "misty",
+ "mixed",
+ "modern",
+ "modest",
+ "moist",
+ "monstrous",
+ "monthly",
+ "monumental",
+ "moral",
+ "mortified",
+ "motherly",
+ "motionless",
+ "mountainous",
+ "muddy",
+ "muffled",
+ "multicolored",
+ "mundane",
+ "murky",
+ "mushy",
+ "musty",
+ "muted",
+ "mysterious",
+ "naive",
+ "narrow",
+ "nasty",
+ "natural",
+ "naughty",
+ "nautical",
+ "near",
+ "neat",
+ "necessary",
+ "needy",
+ "negative",
+ "neglected",
+ "negligible",
+ "neighboring",
+ "nervous",
+ "new",
+ "next",
+ "nice",
+ "nifty",
+ "nimble",
+ "nippy",
+ "nocturnal",
+ "noisy",
+ "nonstop",
+ "normal",
+ "notable",
+ "noted",
+ "noteworthy",
+ "novel",
+ "noxious",
+ "numb",
+ "nutritious",
+ "nutty",
+ "obedient",
+ "obese",
+ "oblong",
+ "oily",
+ "oblong",
+ "obvious",
+ "occasional",
+ "odd",
+ "oddball",
+ "offbeat",
+ "offensive",
+ "official",
+ "old",
+ "old-fashioned",
+ "only",
+ "open",
+ "optimal",
+ "optimistic",
+ "opulent",
+ "orange",
+ "orderly",
+ "organic",
+ "ornate",
+ "ornery",
+ "ordinary",
+ "original",
+ "other",
+ "our",
+ "outlying",
+ "outgoing",
+ "outlandish",
+ "outrageous",
+ "outstanding",
+ "oval",
+ "overcooked",
+ "overdue",
+ "overjoyed",
+ "overlooked",
+ "palatable",
+ "pale",
+ "paltry",
+ "parallel",
+ "parched",
+ "partial",
+ "passionate",
+ "past",
+ "pastel",
+ "peaceful",
+ "peppery",
+ "perfect",
+ "perfumed",
+ "periodic",
+ "perky",
+ "personal",
+ "pertinent",
+ "pesky",
+ "pessimistic",
+ "petty",
+ "phony",
+ "physical",
+ "piercing",
+ "pink",
+ "pitiful",
+ "plain",
+ "plaintive",
+ "plastic",
+ "playful",
+ "pleasant",
+ "pleased",
+ "pleasing",
+ "plump",
+ "plush",
+ "polished",
+ "polite",
+ "political",
+ "pointed",
+ "pointless",
+ "poised",
+ "poor",
+ "popular",
+ "portly",
+ "posh",
+ "positive",
+ "possible",
+ "potable",
+ "powerful",
+ "powerless",
+ "practical",
+ "precious",
+ "present",
+ "prestigious",
+ "pretty",
+ "precious",
+ "previous",
+ "pricey",
+ "prickly",
+ "primary",
+ "prime",
+ "pristine",
+ "private",
+ "prize",
+ "probable",
+ "productive",
+ "profitable",
+ "profuse",
+ "proper",
+ "proud",
+ "prudent",
+ "punctual",
+ "pungent",
+ "puny",
+ "pure",
+ "purple",
+ "pushy",
+ "putrid",
+ "puzzled",
+ "puzzling",
+ "quaint",
+ "qualified",
+ "quarrelsome",
+ "quarterly",
+ "queasy",
+ "querulous",
+ "questionable",
+ "quick",
+ "quick-witted",
+ "quiet",
+ "quintessential",
+ "quirky",
+ "quixotic",
+ "quizzical",
+ "radiant",
+ "ragged",
+ "rapid",
+ "rare",
+ "rash",
+ "raw",
+ "recent",
+ "reckless",
+ "rectangular",
+ "ready",
+ "real",
+ "realistic",
+ "reasonable",
+ "red",
+ "reflecting",
+ "regal",
+ "regular",
+ "reliable",
+ "relieved",
+ "remarkable",
+ "remorseful",
+ "remote",
+ "repentant",
+ "required",
+ "respectful",
+ "responsible",
+ "repulsive",
+ "revolving",
+ "rewarding",
+ "rich",
+ "rigid",
+ "right",
+ "ringed",
+ "ripe",
+ "roasted",
+ "robust",
+ "rosy",
+ "rotating",
+ "rotten",
+ "rough",
+ "round",
+ "rowdy",
+ "royal",
+ "rubbery",
+ "rundown",
+ "ruddy",
+ "rude",
+ "runny",
+ "rural",
+ "rusty",
+ "sad",
+ "safe",
+ "salty",
+ "same",
+ "sandy",
+ "sane",
+ "sarcastic",
+ "sardonic",
+ "satisfied",
+ "scaly",
+ "scarce",
+ "scared",
+ "scary",
+ "scented",
+ "scholarly",
+ "scientific",
+ "scornful",
+ "scratchy",
+ "scrawny",
+ "second",
+ "secondary",
+ "second-hand",
+ "secret",
+ "self-assured",
+ "self-reliant",
+ "selfish",
+ "sentimental",
+ "separate",
+ "serene",
+ "serious",
+ "serpentine",
+ "several",
+ "severe",
+ "shabby",
+ "shadowy",
+ "shady",
+ "shallow",
+ "shameful",
+ "shameless",
+ "sharp",
+ "shimmering",
+ "shiny",
+ "shocked",
+ "shocking",
+ "shoddy",
+ "short",
+ "short-term",
+ "showy",
+ "shrill",
+ "shy",
+ "sick",
+ "silent",
+ "silky",
+ "silly",
+ "silver",
+ "similar",
+ "simple",
+ "simplistic",
+ "sinful",
+ "single",
+ "sizzling",
+ "skeletal",
+ "skinny",
+ "sleepy",
+ "slight",
+ "slim",
+ "slimy",
+ "slippery",
+ "slow",
+ "slushy",
+ "small",
+ "smart",
+ "smoggy",
+ "smooth",
+ "smug",
+ "snappy",
+ "snarling",
+ "sneaky",
+ "sniveling",
+ "snoopy",
+ "sociable",
+ "soft",
+ "soggy",
+ "solid",
+ "somber",
+ "some",
+ "spherical",
+ "sophisticated",
+ "sore",
+ "sorrowful",
+ "soulful",
+ "soupy",
+ "sour",
+ "Spanish",
+ "sparkling",
+ "sparse",
+ "specific",
+ "spectacular",
+ "speedy",
+ "spicy",
+ "spiffy",
+ "spirited",
+ "spiteful",
+ "splendid",
+ "spotless",
+ "spotted",
+ "spry",
+ "square",
+ "squeaky",
+ "squiggly",
+ "stable",
+ "staid",
+ "stained",
+ "stale",
+ "standard",
+ "starchy",
+ "stark",
+ "starry",
+ "steep",
+ "sticky",
+ "stiff",
+ "stimulating",
+ "stingy",
+ "stormy",
+ "straight",
+ "strange",
+ "steel",
+ "strict",
+ "strident",
+ "striking",
+ "striped",
+ "strong",
+ "studious",
+ "stunning",
+ "stupendous",
+ "stupid",
+ "sturdy",
+ "stylish",
+ "subdued",
+ "submissive",
+ "substantial",
+ "subtle",
+ "suburban",
+ "sudden",
+ "sugary",
+ "sunny",
+ "super",
+ "superb",
+ "superficial",
+ "superior",
+ "supportive",
+ "sure-footed",
+ "surprised",
+ "suspicious",
+ "svelte",
+ "sweaty",
+ "sweet",
+ "sweltering",
+ "swift",
+ "sympathetic",
+ "tall",
+ "talkative",
+ "tame",
+ "tan",
+ "tangible",
+ "tart",
+ "tasty",
+ "tattered",
+ "taut",
+ "tedious",
+ "teeming",
+ "tempting",
+ "tender",
+ "tense",
+ "tepid",
+ "terrible",
+ "terrific",
+ "testy",
+ "thankful",
+ "that",
+ "these",
+ "thick",
+ "thin",
+ "third",
+ "thirsty",
+ "this",
+ "thorough",
+ "thorny",
+ "those",
+ "thoughtful",
+ "threadbare",
+ "thrifty",
+ "thunderous",
+ "tidy",
+ "tight",
+ "timely",
+ "tinted",
+ "tiny",
+ "tired",
+ "torn",
+ "total",
+ "tough",
+ "traumatic",
+ "treasured",
+ "tremendous",
+ "tragic",
+ "trained",
+ "tremendous",
+ "triangular",
+ "tricky",
+ "trifling",
+ "trim",
+ "trivial",
+ "troubled",
+ "true",
+ "trusting",
+ "trustworthy",
+ "trusty",
+ "truthful",
+ "tubby",
+ "turbulent",
+ "twin",
+ "ugly",
+ "ultimate",
+ "unacceptable",
+ "unaware",
+ "uncomfortable",
+ "uncommon",
+ "unconscious",
+ "understated",
+ "unequaled",
+ "uneven",
+ "unfinished",
+ "unfit",
+ "unfolded",
+ "unfortunate",
+ "unhappy",
+ "unhealthy",
+ "uniform",
+ "unimportant",
+ "unique",
+ "united",
+ "unkempt",
+ "unknown",
+ "unlawful",
+ "unlined",
+ "unlucky",
+ "unnatural",
+ "unpleasant",
+ "unrealistic",
+ "unripe",
+ "unruly",
+ "unselfish",
+ "unsightly",
+ "unsteady",
+ "unsung",
+ "untidy",
+ "untimely",
+ "untried",
+ "untrue",
+ "unused",
+ "unusual",
+ "unwelcome",
+ "unwieldy",
+ "unwilling",
+ "unwitting",
+ "unwritten",
+ "upbeat",
+ "upright",
+ "upset",
+ "urban",
+ "usable",
+ "used",
+ "useful",
+ "useless",
+ "utilized",
+ "utter",
+ "vacant",
+ "vague",
+ "vain",
+ "valid",
+ "valuable",
+ "vapid",
+ "variable",
+ "vast",
+ "velvety",
+ "venerated",
+ "vengeful",
+ "verifiable",
+ "vibrant",
+ "vicious",
+ "victorious",
+ "vigilant",
+ "vigorous",
+ "villainous",
+ "violet",
+ "violent",
+ "virtual",
+ "virtuous",
+ "visible",
+ "vital",
+ "vivacious",
+ "vivid",
+ "voluminous",
+ "wan",
+ "warlike",
+ "warm",
+ "warmhearted",
+ "warped",
+ "wary",
+ "wasteful",
+ "watchful",
+ "waterlogged",
+ "watery",
+ "wavy",
+ "wealthy",
+ "weak",
+ "weary",
+ "webbed",
+ "wee",
+ "weekly",
+ "weepy",
+ "weighty",
+ "weird",
+ "welcome",
+ "well-documented",
+ "well-groomed",
+ "well-informed",
+ "well-lit",
+ "well-made",
+ "well-off",
+ "well-to-do",
+ "well-worn",
+ "wet",
+ "which",
+ "whimsical",
+ "whirlwind",
+ "whispered",
+ "white",
+ "whole",
+ "whopping",
+ "wicked",
+ "wide",
+ "wide-eyed",
+ "wiggly",
+ "wild",
+ "willing",
+ "wilted",
+ "winding",
+ "windy",
+ "winged",
+ "wiry",
+ "wise",
+ "witty",
+ "wobbly",
+ "woeful",
+ "wonderful",
+ "wooden",
+ "woozy",
+ "wordy",
+ "worldly",
+ "worn",
+ "worried",
+ "worrisome",
+ "worse",
+ "worst",
+ "worthless",
+ "worthwhile",
+ "worthy",
+ "wrathful",
+ "wretched",
+ "writhing",
+ "wrong",
+ "wry",
+ "yawning",
+ "yearly",
+ "yellow",
+ "yellowish",
+ "young",
+ "youthful",
+ "yummy",
+ "zany",
+ "zealous",
+ "zesty",
+ "zigzag",
+]
+
+export function getRandomUsername(): { first: string, second: string } {
+ const n = Math.floor(Math.random() * noun.length)
+ const a = Math.floor(Math.random() * adj.length)
+ return {
+ first: adj[a],
+ second: noun[n]
+ }
+}
+
+export function getRandomPassword(): string {
+ if (bankUiSettings.simplePasswordForRandomAccounts) return "123"
+ return encodeCrock(getRandomBytes(16))
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/scss/DurationPicker.scss b/packages/demobank-ui/src/scss/DurationPicker.scss
deleted file mode 100644
index aa75b9916..000000000
--- a/packages/demobank-ui/src/scss/DurationPicker.scss
+++ /dev/null
@@ -1,70 +0,0 @@
-.rdp-picker {
- display: flex;
- height: 175px;
-}
-
-@media (max-width: 400px) {
- .rdp-picker {
- width: 250px;
- }
-}
-
-.rdp-masked-div {
- overflow: hidden;
- height: 175px;
- position: relative;
-}
-
-.rdp-column-container {
- flex-grow: 1;
- display: inline-block;
-}
-
-.rdp-column {
- position: absolute;
- z-index: 0;
- width: 100%;
-}
-
-.rdp-reticule {
- border: 0;
- border-top: 2px solid rgba(109, 202, 236, 1);
- height: 2px;
- position: absolute;
- width: 80%;
- margin: 0;
- z-index: 100;
- left: 50%;
- -webkit-transform: translateX(-50%);
- transform: translateX(-50%);
-}
-
-.rdp-text-overlay {
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 35px;
- font-size: 20px;
- left: 50%;
- -webkit-transform: translateX(-50%);
- transform: translateX(-50%);
-}
-
-.rdp-cell div {
- font-size: 17px;
- color: gray;
- font-style: italic;
-}
-
-.rdp-cell {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 35px;
- font-size: 18px;
-}
-
-.rdp-center {
- font-size: 25px;
-}
diff --git a/packages/demobank-ui/src/scss/_aside.scss b/packages/demobank-ui/src/scss/_aside.scss
deleted file mode 100644
index 11809990b..000000000
--- a/packages/demobank-ui/src/scss/_aside.scss
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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)
- */
-
-html {
- &.has-aside-left {
- &.has-aside-expanded {
- nav.navbar,
- body {
- padding-left: $aside-width;
- }
- }
- aside.is-placed-left {
- display: block;
- }
- }
-}
-
-aside.aside.is-expanded {
- width: $aside-width;
-
- .menu-list {
- @include icon-with-update-mark($aside-icon-width);
-
- span.menu-item-label {
- display: inline-block;
- }
-
- li.is-active {
- ul {
- display: block;
- }
- background-color: $body-background-color;
- }
- }
-}
-
-aside.aside {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- z-index: 40;
- height: 100vh;
- padding: 0;
- box-shadow: $aside-box-shadow;
- background: $aside-background-color;
-
- .aside-tools {
- display: flex;
- flex-direction: row;
- width: 100%;
- background-color: $aside-tools-background-color;
- color: $aside-tools-color;
- line-height: $navbar-height;
- height: $navbar-height;
- padding-left: $default-padding * 0.5;
- flex: 1;
-
- .icon {
- margin-right: $default-padding * 0.5;
- }
- }
-
- .menu-list {
- li {
- a {
- &.has-dropdown-icon {
- position: relative;
- padding-right: $aside-icon-width;
-
- .dropdown-icon {
- position: absolute;
- top: $size-base * 0.5;
- right: 0;
- }
- }
- }
- ul {
- display: none;
- border-left: 0;
- background-color: darken($base-color, 2.5%);
- padding-left: 0;
- margin: 0 0 $default-padding * 0.5;
-
- li {
- a {
- padding: $default-padding * 0.5 0 $default-padding * 0.5
- $default-padding * 0.5;
- font-size: $aside-submenu-font-size;
-
- &.has-icon {
- padding-left: 0;
- }
- &.is-active {
- &:not(:hover) {
- background: transparent;
- }
- }
- }
- }
- }
- }
- }
-
- .menu-label {
- padding: 0 $default-padding * 0.5;
- margin-top: $default-padding * 0.5;
- margin-bottom: $default-padding * 0.5;
- }
-}
diff --git a/packages/demobank-ui/src/scss/_card.scss b/packages/demobank-ui/src/scss/_card.scss
deleted file mode 100644
index 3f71aeb6a..000000000
--- a/packages/demobank-ui/src/scss/_card.scss
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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)
- */
-
-.card:not(:last-child) {
- margin-bottom: $default-padding;
-}
-
-.card {
- border-radius: $radius-large;
- border: $card-border;
-
- &.has-table {
- .card-content {
- padding: 0;
- }
- .b-table {
- border-radius: $radius-large;
- overflow: hidden;
- }
- }
-
- &.is-card-widget {
- .card-content {
- padding: $default-padding * 0.5;
- }
- }
-
- .card-header {
- border-bottom: 1px solid $base-color-light;
- }
-
- .card-content {
- hr {
- margin-left: $card-content-padding * -1;
- margin-right: $card-content-padding * -1;
- }
- }
-
- .is-widget-icon {
- .icon {
- width: 5rem;
- height: 5rem;
- }
- }
-
- .is-widget-label {
- .subtitle {
- color: $grey;
- }
- }
-}
diff --git a/packages/demobank-ui/src/scss/_custom-calendar.scss b/packages/demobank-ui/src/scss/_custom-calendar.scss
deleted file mode 100644
index 463cd88d3..000000000
--- a/packages/demobank-ui/src/scss/_custom-calendar.scss
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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/>
- */
-
-:root {
- --primary-color: #3298dc;
-
- --primary-text-color-dark: rgba(0, 0, 0, 0.87);
- --secondary-text-color-dark: rgba(0, 0, 0, 0.57);
- --disabled-text-color-dark: rgba(0, 0, 0, 0.13);
-
- --primary-text-color-light: rgba(255, 255, 255, 0.87);
- --secondary-text-color-light: rgba(255, 255, 255, 0.57);
- --disabled-text-color-light: rgba(255, 255, 255, 0.13);
-
- --font-stack: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
-
- --primary-card-color: #fff;
- --primary-background-color: #f2f2f2;
-
- --box-shadow-lvl-1: 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
- --box-shadow-lvl-2: 0 3px 6px rgba(0, 0, 0, 0.16),
- 0 3px 6px rgba(0, 0, 0, 0.23);
- --box-shadow-lvl-3: 0 10px 20px rgba(0, 0, 0, 0.19),
- 0 6px 6px rgba(0, 0, 0, 0.23);
- --box-shadow-lvl-4: 0 14px 28px rgba(0, 0, 0, 0.25),
- 0 10px 10px rgba(0, 0, 0, 0.22);
-}
-
-.home .datePicker div {
- margin-top: 0px;
- margin-bottom: 0px;
-}
-.datePicker {
- text-align: left;
- background: var(--primary-card-color);
- border-radius: 3px;
- z-index: 200;
- position: fixed;
- height: auto;
- max-height: 90vh;
- width: 90vw;
- max-width: 448px;
- transform-origin: top left;
- transition: transform 0.22s ease-in-out, opacity 0.22s ease-in-out;
- top: 50%;
- left: 50%;
- opacity: 0;
- transform: scale(0) translate(-50%, -50%);
- user-select: none;
-
- &.datePicker--opened {
- opacity: 1;
- transform: scale(1) translate(-50%, -50%);
- }
-
- .datePicker--titles {
- border-top-left-radius: 3px;
- border-top-right-radius: 3px;
- padding: 24px;
- height: 100px;
- background: var(--primary-color);
-
- h2,
- h3 {
- cursor: pointer;
- color: #fff;
- line-height: 1;
- padding: 0;
- margin: 0;
- font-size: 32px;
- }
-
- h3 {
- color: rgba(255, 255, 255, 0.57);
- font-size: 18px;
- padding-bottom: 2px;
- }
- }
-
- nav {
- padding: 20px;
- height: 56px;
-
- h4 {
- width: calc(100% - 60px);
- text-align: center;
- display: inline-block;
- padding: 0;
- font-size: 14px;
- line-height: 24px;
- margin: 0;
- position: relative;
- top: -9px;
- color: var(--primary-text-color);
- }
-
- i {
- cursor: pointer;
- color: var(--secondary-text-color);
- font-size: 26px;
- user-select: none;
- border-radius: 50%;
-
- &:hover {
- background: var(--disabled-text-color-dark);
- }
- }
- }
-
- .datePicker--scroll {
- overflow-y: auto;
- max-height: calc(90vh - 56px - 100px);
- }
-
- .datePicker--calendar {
- padding: 0 20px;
-
- .datePicker--dayNames {
- width: 100%;
- display: grid;
- text-align: center;
-
- // there's probably a better way to do this, but wanted to try out CSS grid
- grid-template-columns:
- calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
- calc(100% / 7) calc(100% / 7) calc(100% / 7);
-
- span {
- color: var(--secondary-text-color-dark);
- font-size: 14px;
- line-height: 42px;
- display: inline-grid;
- }
- }
-
- .datePicker--days {
- width: 100%;
- display: grid;
- text-align: center;
- grid-template-columns:
- calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
- calc(100% / 7) calc(100% / 7) calc(100% / 7);
-
- span {
- color: var(--primary-text-color-dark);
- line-height: 42px;
- font-size: 14px;
- display: inline-grid;
- transition: color 0.22s;
- height: 42px;
- position: relative;
- cursor: pointer;
- user-select: none;
- border-radius: 50%;
-
- &::before {
- content: "";
- position: absolute;
- z-index: -1;
- height: 42px;
- width: 42px;
- left: calc(50% - 21px);
- background: var(--primary-color);
- border-radius: 50%;
- transition: transform 0.22s, opacity 0.22s;
- transform: scale(0);
- opacity: 0;
- }
-
- &[disabled="true"] {
- cursor: unset;
- }
-
- &.datePicker--today {
- font-weight: 700;
- }
-
- &.datePicker--selected {
- color: rgba(255, 255, 255, 0.87);
-
- &:before {
- transform: scale(1);
- opacity: 1;
- }
- }
- }
- }
- }
-
- .datePicker--selectYear {
- padding: 0 20px;
- display: block;
- width: 100%;
- text-align: center;
- max-height: 362px;
-
- span {
- display: block;
- width: 100%;
- font-size: 24px;
- margin: 20px auto;
- cursor: pointer;
-
- &.selected {
- font-size: 42px;
- color: var(--primary-color);
- }
- }
- }
-
- div.datePicker--actions {
- width: 100%;
- padding: 8px;
- text-align: right;
-
- button {
- margin-bottom: 0;
- font-size: 15px;
- cursor: pointer;
- color: var(--primary-text-color);
- border: none;
- margin-left: 8px;
- min-width: 64px;
- line-height: 36px;
- background-color: transparent;
- appearance: none;
- padding: 0 16px;
- border-radius: 3px;
- transition: background-color 0.13s;
-
- &:hover,
- &:focus {
- outline: none;
- background-color: var(--disabled-text-color-dark);
- }
- }
- }
-}
-
-.datePicker--background {
- z-index: 199;
- position: fixed;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- background: rgba(0, 0, 0, 0.52);
- animation: fadeIn 0.22s forwards;
-}
diff --git a/packages/demobank-ui/src/scss/_form.scss b/packages/demobank-ui/src/scss/_form.scss
deleted file mode 100644
index 9d93477fd..000000000
--- a/packages/demobank-ui/src/scss/_form.scss
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- 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)
- */
-
-.field {
- &.has-check {
- .field-body {
- margin-top: $default-padding * 0.125;
- }
- }
- .control {
- .mdi-24px.mdi-set,
- .mdi-24px.mdi:before {
- font-size: inherit;
- }
- }
-}
-.upload {
- .upload-draggable {
- display: block;
- }
-}
-
-.input,
-.textarea,
-select {
- box-shadow: none;
-
- &:focus,
- &:active {
- box-shadow: none !important;
- }
-}
-
-.switch input[type="checkbox"] + .check:before {
- box-shadow: none;
-}
-
-.switch,
-.b-checkbox.checkbox {
- input[type="checkbox"] {
- &:focus + .check,
- &:focus:checked + .check {
- box-shadow: none !important;
- }
- }
-}
-
-.b-checkbox.checkbox input[type="checkbox"],
-.b-radio.radio input[type="radio"] {
- & + .check {
- border: $checkbox-border;
- }
-}
diff --git a/packages/demobank-ui/src/scss/_hero-bar.scss b/packages/demobank-ui/src/scss/_hero-bar.scss
deleted file mode 100644
index 31b7e623e..000000000
--- a/packages/demobank-ui/src/scss/_hero-bar.scss
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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)
- */
-
-section.hero.is-hero-bar {
- background-color: $hero-bar-background;
- border-bottom: $light-border;
-
- .hero-body {
- padding: $default-padding;
-
- .level-item {
- &.is-hero-avatar-item {
- margin-right: $default-padding;
- }
-
- > div > .level {
- margin-bottom: $default-padding * 0.5;
- }
-
- .subtitle + p {
- margin-top: $default-padding * 0.5;
- }
- }
-
- .button {
- &.is-hero-button {
- background-color: rgba($white, 0.5);
- font-weight: 300;
- @include transition(background-color);
-
- &:hover {
- background-color: $white;
- }
- }
- }
- }
-}
diff --git a/packages/demobank-ui/src/scss/_loading.scss b/packages/demobank-ui/src/scss/_loading.scss
deleted file mode 100644
index d25bf8048..000000000
--- a/packages/demobank-ui/src/scss/_loading.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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/>
- */
-
-.lds-ring {
- display: inline-block;
- position: relative;
- width: 80px;
- height: 80px;
-}
-.lds-ring div {
- box-sizing: border-box;
- display: block;
- position: absolute;
- width: 64px;
- height: 64px;
- margin: 8px;
- border: 8px solid black;
- border-radius: 50%;
- animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
- border-color: black transparent transparent transparent;
-}
-.lds-ring div:nth-child(1) {
- animation-delay: -0.45s;
-}
-.lds-ring div:nth-child(2) {
- animation-delay: -0.3s;
-}
-.lds-ring div:nth-child(3) {
- animation-delay: -0.15s;
-}
-@keyframes lds-ring {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
diff --git a/packages/demobank-ui/src/scss/_main-section.scss b/packages/demobank-ui/src/scss/_main-section.scss
deleted file mode 100644
index 01edc24bf..000000000
--- a/packages/demobank-ui/src/scss/_main-section.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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)
- */
-
-section.section.is-main-section {
- padding-top: $default-padding;
-}
diff --git a/packages/demobank-ui/src/scss/_nav-bar.scss b/packages/demobank-ui/src/scss/_nav-bar.scss
deleted file mode 100644
index c6dd04263..000000000
--- a/packages/demobank-ui/src/scss/_nav-bar.scss
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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)
- */
-
-nav.navbar {
- box-shadow: $navbar-box-shadow;
-
- .navbar-item {
- &.has-user-avatar {
- .is-user-avatar {
- margin-right: $default-padding * 0.5;
- display: inline-flex;
- width: $navbar-avatar-size;
- height: $navbar-avatar-size;
- }
- }
-
- &.has-divider {
- border-right: $navbar-divider-border;
- }
-
- &.no-left-space {
- padding-left: 0;
- }
-
- &.has-dropdown {
- padding-right: 0;
- padding-left: 0;
-
- .navbar-link {
- padding-right: $navbar-item-h-padding;
- padding-left: $navbar-item-h-padding;
- }
- }
-
- &.has-control {
- padding-top: 0;
- padding-bottom: 0;
- }
-
- .control {
- .input {
- color: $navbar-input-color;
- border: 0;
- box-shadow: none;
- background: transparent;
-
- &::placeholder {
- color: $navbar-input-placeholder-color;
- }
- }
- }
- }
-}
-
-@include touch {
- nav.navbar {
- display: flex;
- padding-right: 0;
-
- .navbar-brand {
- flex: 1;
-
- &.is-right {
- flex: none;
- }
- }
-
- .navbar-item {
- &.no-left-space-touch {
- padding-left: 0;
- }
- }
-
- .navbar-menu {
- position: absolute;
- width: 100vw;
- padding-top: 0;
- top: $navbar-height;
- left: 0;
-
- .navbar-item {
- .icon:first-child {
- margin-right: $default-padding * 0.5;
- }
-
- &.has-dropdown {
- > .navbar-link {
- background-color: $white-ter;
- .icon:last-child {
- display: none;
- }
- }
- }
-
- &.has-user-avatar {
- > .navbar-link {
- display: flex;
- align-items: center;
- padding-top: $default-padding * 0.5;
- padding-bottom: $default-padding * 0.5;
- }
- }
- }
- }
- }
-}
-
-@include desktop {
- nav.navbar {
- .navbar-item {
- padding-right: $navbar-item-h-padding;
- padding-left: $navbar-item-h-padding;
-
- &:not(.is-desktop-icon-only) {
- .icon:first-child {
- margin-right: $default-padding * 0.5;
- }
- }
- &.is-desktop-icon-only {
- span:not(.icon) {
- display: none;
- }
- }
- }
- }
-}
diff --git a/packages/demobank-ui/src/scss/_table.scss b/packages/demobank-ui/src/scss/_table.scss
deleted file mode 100644
index b68d50e4f..000000000
--- a/packages/demobank-ui/src/scss/_table.scss
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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)
- */
-
-table.table {
- thead {
- th {
- border-bottom-width: 1px;
- }
- }
-
- td,
- th {
- &.checkbox-cell {
- .b-checkbox.checkbox:not(.button) {
- margin-right: 0;
- width: 20px;
-
- .control-label {
- display: none;
- padding: 0;
- }
- }
- }
- }
-
- td {
- .image {
- margin: 0 auto;
- width: $table-avatar-size;
- height: $table-avatar-size;
- }
-
- &.is-progress-col {
- min-width: 5rem;
- vertical-align: middle;
- }
- }
-}
-
-.b-table {
- .table {
- border: 0;
- border-radius: 0;
- }
-
- /* This stylizes buefy's pagination */
- .table-wrapper {
- margin-bottom: 0;
- }
-
- .table-wrapper + .level {
- padding: $notification-padding;
- padding-left: $card-content-padding;
- padding-right: $card-content-padding;
- margin: 0;
- border-top: $base-color-light;
- background: $notification-background-color;
-
- .pagination-link {
- background: $button-background-color;
- color: $button-color;
- border-color: $button-border-color;
-
- &.is-current {
- border-color: $button-active-border-color;
- }
- }
-
- .pagination-previous,
- .pagination-next,
- .pagination-link {
- border-color: $button-border-color;
- color: $base-color;
-
- &[disabled] {
- background-color: transparent;
- }
- }
- }
-}
-
-@include mobile {
- .card {
- &.has-table {
- .b-table {
- .table-wrapper + .level {
- .level-left + .level-right {
- margin-top: 0;
- }
- }
- }
- }
- &.has-mobile-sort-spaced {
- .b-table {
- .field.table-mobile-sort {
- padding-top: $default-padding * 0.5;
- }
- }
- }
- }
- .b-table {
- .field.table-mobile-sort {
- padding: 0 $default-padding * 0.5;
- }
-
- .table-wrapper.has-mobile-cards {
- tr {
- box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
- margin-bottom: 3px !important;
- }
- td {
- &.is-progress-col {
- span,
- progress {
- display: flex;
- width: 45%;
- align-items: center;
- align-self: center;
- }
- }
-
- &.checkbox-cell,
- &.is-image-cell {
- border-bottom: 0 !important;
- }
-
- &.checkbox-cell,
- &.is-actions-cell {
- &:before {
- display: none;
- }
- }
-
- &.has-no-head-mobile {
- &:before {
- display: none;
- }
-
- span {
- display: block;
- width: 100%;
- }
-
- &.is-progress-col {
- progress {
- width: 100%;
- }
- }
-
- &.is-image-cell {
- .image {
- width: $table-avatar-size-mobile;
- height: auto;
- margin: 0 auto $default-padding * 0.25;
- }
- }
- }
- }
- }
- }
-}
diff --git a/packages/demobank-ui/src/scss/_theme-default.scss b/packages/demobank-ui/src/scss/_theme-default.scss
deleted file mode 100644
index 538dfd4da..000000000
--- a/packages/demobank-ui/src/scss/_theme-default.scss
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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)
- */
-
-/* We'll need some initial vars to use here */
-@import "node_modules/bulma/sass/utilities/initial-variables";
-
-/* Base: Size */
-$size-base: 1rem;
-$default-padding: $size-base * 1.5;
-
-/* Default font */
-$family-sans-serif: "Nunito", sans-serif;
-
-/* Base color */
-$base-color: #2e323a;
-$base-color-light: rgba(24, 28, 33, 0.06);
-
-/* General overrides */
-$primary: $turquoise;
-$body-background-color: #f8f8f8;
-$link: $blue;
-$link-visited: $purple;
-$light-border: 1px solid $base-color-light;
-$hr-height: 1px;
-
-/* NavBar: specifics */
-$navbar-input-color: $grey-darker;
-$navbar-input-placeholder-color: $grey-lighter;
-$navbar-box-shadow: 0 1px 0 rgba(24, 28, 33, 0.04);
-$navbar-divider-border: 1px solid rgba($grey-lighter, 0.25);
-$navbar-item-h-padding: $default-padding * 0.75;
-$navbar-avatar-size: 1.75rem;
-
-/* Aside: Bulma override */
-$menu-item-radius: 0;
-$menu-list-link-padding: $size-base * 0.5 0;
-$menu-label-color: lighten($base-color, 25%);
-$menu-item-color: lighten($base-color, 30%);
-$menu-item-hover-color: $white;
-$menu-item-hover-background-color: darken($base-color, 3.5%);
-$menu-item-active-color: $white;
-$menu-item-active-background-color: darken($base-color, 2.5%);
-
-/* Aside: specifics */
-$aside-width: $size-base * 14;
-$aside-mobile-width: $size-base * 15;
-$aside-icon-width: $size-base * 3;
-$aside-submenu-font-size: $size-base * 0.95;
-$aside-box-shadow: none;
-$aside-background-color: $base-color;
-$aside-tools-background-color: darken($aside-background-color, 10%);
-$aside-tools-color: $white;
-
-/* Title Bar: specifics */
-$title-bar-color: $grey;
-$title-bar-active-color: $black-ter;
-
-/* Hero Bar: specifics */
-$hero-bar-background: $white;
-
-/* Card: Bulma override */
-$card-shadow: none;
-$card-header-shadow: none;
-
-/* Card: specifics */
-$card-border: 1px solid $base-color-light;
-$card-header-border-bottom-color: $base-color-light;
-
-/* Table: Bulma override */
-$table-cell-border: 1px solid $white-bis;
-
-/* Table: specifics */
-$table-avatar-size: $size-base * 1.5;
-$table-avatar-size-mobile: 25vw;
-
-/* Form */
-$checkbox-border: 1px solid $base-color;
-
-/* Modal card: Bulma override */
-$modal-card-head-background-color: $white-ter;
-$modal-card-title-size: $size-base;
-$modal-card-body-padding: $default-padding 20px;
-$modal-card-head-border-bottom: 1px solid $white-ter;
-$modal-card-foot-border-top: 0;
-
-/* Modal card: specifics */
-$modal-card-width: 80vw;
-$modal-card-width-mobile: 90vw;
-$modal-card-foot-background-color: $white-ter;
-
-/* Notification: Bulma override */
-$notification-padding: $default-padding * 0.75 $default-padding;
-
-/* Footer: Bulma override */
-$footer-background-color: $white;
-$footer-padding: $default-padding * 0.33 $default-padding;
-
-/* Footer: specifics */
-$footer-logo-height: $size-base * 2;
-
-/* Progress: Bulma override */
-$progress-bar-background-color: $grey-lighter;
-
-/* Icon: specifics */
-$icon-update-mark-size: $size-base * 0.5;
-$icon-update-mark-color: $yellow;
-
-$input-disabled-border-color: $grey-lighter;
-$table-row-hover-background-color: hsl(0, 0%, 80%);
-
-.menu-list {
- div {
- border-radius: $menu-item-radius;
- color: $menu-item-color;
- display: block;
- padding: $menu-list-link-padding;
- }
-}
diff --git a/packages/demobank-ui/src/scss/bank.scss b/packages/demobank-ui/src/scss/bank.scss
deleted file mode 100644
index f8de0a984..000000000
--- a/packages/demobank-ui/src/scss/bank.scss
+++ /dev/null
@@ -1,353 +0,0 @@
-.navcontainer:not(.default-navcontainer) {
- margin-bottom: 0 !important;
-}
-
-.abort-button {
- margin-left: 2px;
- border: 2px solid rgb(0, 120, 231);
- color: rgb(0, 120, 231);
- font-size: 87%;
- margin-top: 1px;
- background: white;
-}
-
-div.pages-list {
- margin-top: 15px;
-}
-
-.footer {
- margin-left: 2em;
- margin-right: 2em;
-}
-
-.qr-div,
-.login-div,
-.register-div {
- display: block;
- text-align: center;
-}
-
-a.page-number {
- color: blue;
-}
-
-a.current-page-number {
- color: inherit;
- background-color: inherit;
-}
-
-.cancelled {
- text-decoration: line-through;
-}
-
-input[type="number"]::-webkit-outer-spin-button,
-input[type="number"]::-webkit-inner-spin-button {
- -webkit-appearance: none;
- margin: 0;
-}
-
-/* This CSS code styles the tab */
-.tab {
- overflow: hidden;
-}
-
-.top-right {
- float: right;
-}
-.some-space {
- display: inline-block;
- border: 20px;
- margin-right: 15px;
- margin-top: 15px;
-}
-
-.tab button {
- background-color: lightgray;
- color: black;
- float: left;
- border: none;
- outline: none;
- cursor: pointer;
- padding: 18px 19px;
- border: 2px solid #c1c1c1;
- transition: 0.5s;
- font-weight: bold;
-}
-
-.tab button:hover {
- background-color: yellow;
- border: 2px solid #c1c1c1;
- color: black;
-}
-
-.tab button.active {
- background-color: orange;
- border: 2px solid #c1c1c1;
- color: black;
- font-weight: bold;
-}
-
-.tabcontent {
- display: none;
- padding: 8px 16px;
- border: 2px solid #c1c1c1;
- width: min-content;
-}
-
-.tabcontent.active {
- display: block;
-}
-
-input[type="number"] {
- -moz-appearance: textfield;
-}
-
-#transfer-fields {
- display: flex;
- flex-wrap: wrap;
-}
-
-#id_amount {
- width: 6em;
- display: inline-block;
- border-radius: 4px 0px 0px 4px;
-}
-
-/**
- * Amount without the currency,
- * placed left to a .currency-indicator.
- */
-#main .amount {
- width: 6em;
- display: inline-block;
- border-radius: 4px 0px 0px 4px;
-}
-
-input {
- background-color: inherit;
-}
-
-.large-amount {
- font-weight: bold;
- font-size: xxx-large;
-}
-
-.currency {
- font-style: oblique;
-}
-
-/*
- * Currency indicator to the right of input fields,
- * with non-rounded corners to the left.
- */
-#main .currency-indicator {
- color: black;
- border-radius: 4px 0px 0px 4px;
- position: relative;
-}
-
-#main .fieldlabel {
- display: block;
- padding-bottom: 0.5em;
-}
-
-#main .fieldbox {
- margin-right: 1em;
- margin-bottom: 0.5em;
-}
-
-#logout-button {
- display: block;
- width: fit-content;
-}
-
-.register-form > .pure-form,
-.login-form > .pure-form {
- background: #4a4a4a;
- color: #ffffff;
- display: inline-block;
- text-align: left;
- margin-left: auto;
- margin-right: auto;
- padding: 16px 16px;
- border-radius: 8px;
- width: min-content;
- .formFieldLabel {
- margin: 2px 2px;
- }
- input[type="text"],
- input[type="password"] {
- border: none;
- border-radius: 4px;
- background: #6a6a6a;
- color: #fefefe;
- box-shadow: none;
- }
- input[placeholder="Password"][type="password"] {
- margin-bottom: 8px;
- }
- .btn-register,
- .btn-login {
- float: left;
- }
- .btn-cancel {
- float: right;
- }
- h2 {
- margin-top: 0;
- margin-bottom: 10px;
- }
-}
-
-.challenge-div {
- display: block;
- text-align: center;
-}
-
-.challenge-form > .pure-form {
- background: #4a4a4a;
- color: #ffffff;
- display: inline-block;
- text-align: left;
- margin-left: auto;
- margin-right: auto;
- padding: 16px 16px;
- border-radius: 8px;
- width: min-content;
- .formFieldLabel {
- margin: 2px 2px;
- }
- input[type="text"] {
- border: none;
- border-radius: 4px;
- background: #6a6a6a;
- color: #fefefe;
- box-shadow: none;
- }
- .btn-confirm {
- float: left;
- }
- .btn-cancel {
- float: right;
- }
- h2 {
- margin-top: 0;
- margin-bottom: 10px;
- }
-}
-
-.wire-transfer-form > .pure-form,
-.payto-form > .pure-form,
-.reserve-form > .pure-form {
- background: #4a4a4a;
- color: #ffffff;
- display: inline-block;
- text-align: left;
- margin-left: auto;
- margin-right: auto;
- padding: 16px 16px;
- border-radius: 8px;
- width: min-content;
- .formFieldLabel {
- margin: 2px 2px;
- }
- input[type="text"] {
- border: none;
- border-radius: 4px;
- background: #6a6a6a;
- color: #fefefe;
- box-shadow: none;
- }
-}
-
-html {
- background: #ffffff;
- color: #2a2a2a;
-}
-
-.hint {
- scale: 0.7;
-}
-h1.nav {
- text-align: center;
-}
-
-.pure-form > fieldset > label {
- display: block;
-}
-.pure-form > fieldset > input[disabled] {
- color: black !important;
-}
-.pure-form > fieldset > div > input[disabled] {
- color: black !important;
-}
-
-.pure-form > fieldset > div.channel > div {
- display: inline-block;
- margin: 1em;
- border: 1px black solid;
- width: fit-content;
- padding: 0.4em;
- cursor: pointer;
-}
-
-.button-success {
- background: rgb(28, 184, 65);
- /* this is a green */
-}
-
-.button-error {
- background: rgb(202, 60, 60);
- /* this is a maroon */
-}
-
-.button-warning {
- background: rgb(223, 117, 20);
- /* this is an orange */
-}
-
-.button-secondary {
- background: rgb(66, 184, 221);
- /* this is a light blue */
-}
-
-[name=wire-transfer-form] > input {
- margin-bottom: 1em;
-
-}
-
-.lds-ring {
- display: inline-block;
- position: relative;
- width: 80px;
- height: 80px;
-}
-.lds-ring div {
- box-sizing: border-box;
- display: block;
- position: absolute;
- width: 64px;
- height: 64px;
- margin: 8px;
- border: 8px solid black;
- border-radius: 50%;
- animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
- border-color: black transparent transparent transparent;
-}
-.lds-ring div:nth-child(1) {
- animation-delay: -0.45s;
-}
-.lds-ring div:nth-child(2) {
- animation-delay: -0.3s;
-}
-.lds-ring div:nth-child(3) {
- animation-delay: -0.15s;
-}
-@keyframes lds-ring {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
diff --git a/packages/demobank-ui/src/scss/colors-bank.scss b/packages/demobank-ui/src/scss/colors-bank.scss
deleted file mode 100644
index e11bbe203..000000000
--- a/packages/demobank-ui/src/scss/colors-bank.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-nav,
-nav a,
-nav span,
-.navcontainer,
-nav button,
-.demobar,
-.navbtn {
- color: white;
- background: #a00000;
-}
-
-nav a.active,
-nav button,
-nav span.active,
-.navbtn.active {
- background-color: #7a0606;
-}
-
-nav a.active:hover,
-nav span.active:hover,
-.navbtn.active:hover,
-nav button:hover,
-nav a:hover,
-nav span:hover,
-.navbtn:hover {
- background: #df3d3d;
-}
-
-nav a.navbtn.langbtn:focus {
- background-color: #df3d3d;
-}
diff --git a/packages/demobank-ui/src/scss/demo.scss b/packages/demobank-ui/src/scss/demo.scss
deleted file mode 100644
index c2d9fa903..000000000
--- a/packages/demobank-ui/src/scss/demo.scss
+++ /dev/null
@@ -1,167 +0,0 @@
-@charset "UTF-8";
-/*
-Style common to all demo pages.
-
-Colors:
-- #1e2739 (dark blue)
-- #0042b2 (default blue)
-- #3daee9 (highlight blue)
-*/
-
-.demobar h1 {
- text-align: center;
-}
-
-.demobar > p {
- padding: 0.5em;
-}
-
-.demobar a,
-.demobar a:visited {
- color: inherit;
- background-color: inherit;
-}
-
-.tt {
- font-family: "Lucida Console", Monaco, monospace;
-}
-
-.informational-ok {
- background: lightgreen;
- border-radius: 1em;
- padding: 0.5em;
-}
-
-.informational-fail {
- background: lightpink;
- border-radius: 1em;
- padding: 0.5em;
-}
-
-.content {
- margin-left: 1em;
- margin-right: 1em;
- overflow-x: auto;
-}
-
-.demobar {
- overflow-x: auto;
- background-color: #0042b2;
- color: white;
-}
-
-body {
- overflow-x: hidden;
- overflow-y: auto;
-}
-
-.navcontainer {
- background: #0042b2;
- margin-bottom: 50px;
- width: 100%;
- color: white;
- // position: -webkit-sticky;
- // position: sticky;
- top: 0px;
- width: 100vw;
- backdrop-filter: blur(10px);
- opacity: 1;
- z-index: 100;
-}
-
-nav {
- // left: 1vw;
- position: relative;
- background: #0042b2;
- z-index: 100;
-}
-
-nav a,
-nav button,
-nav span,
-.navbtn {
- border: none;
- color: white;
- text-align: center;
- // text-decoration: none;
- display: inline-block;
- font-size: 16px;
- background: #0042b2;
- height: inherit;
-}
-
-nav a,
-nav button,
-nav span,
-.navbtn {
- padding: 8px;
-}
-
-
-nav a:hover,
-nav span:hover,
-.navbtn:hover {
- background: #3daee9;
-}
-
-nav a.active,
-nav span.active,
-.navbtn.active {
- background-color: #1e2739;
-}
-
-nav a.active:hover,
-nav button.active:hover,
-nav span.active:hover,
-.navbtn.active:hover {
- background: #3daee9;
-}
-
-nav a,
-nav span,
-.navbtn {
- cursor: pointer;
-}
-
-nav .right {
- float: right;
- margin-right: 5vw;
-}
-nav .hide div.nav {
- display: none;
-}
-// nav .right div.nav:hover {
-// display: block;
-// }
-
-// nav .right:hover div.nav {
-// display: block;
-// }
-
-.langbtn {
- width: 100px;
- text-align: left;
-}
-
-.skip {
- position: absolute;
- left: -10000px;
- top: auto;
- width: 1px;
- height: 1px;
- overflow: hidden;
-}
-
-.skip:focus {
- position: static;
- width: auto;
- height: auto;
-}
-
-.demolist > a {
- margin: 8px;
-}
-
-.buttons-account input.pure-button {
- margin: 8px;
-} \ No newline at end of file
diff --git a/packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf b/packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
deleted file mode 100644
index 7665ee336..000000000
--- a/packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/fonts/nunito.css b/packages/demobank-ui/src/scss/fonts/nunito.css
deleted file mode 100644
index 8d45df9a1..000000000
--- a/packages/demobank-ui/src/scss/fonts/nunito.css
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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/>
- */
-
-@font-face {
- font-family: "Nunito";
- font-style: normal;
- font-weight: 400;
- src: url(./XRXV3I6Li01BKofINeaE.ttf) format("truetype");
-}
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot b/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
deleted file mode 100644
index ab6b25ded..000000000
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf b/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
deleted file mode 100644
index 824be10fa..000000000
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff b/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
deleted file mode 100644
index 7e087c1de..000000000
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2 b/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
deleted file mode 100644
index b5caa4ddc..000000000
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css b/packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
deleted file mode 100644
index 2b8a2b244..000000000
--- a/packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
+++ /dev/null
@@ -1,15109 +0,0 @@
-@font-face {
- font-family: "Material Design Icons";
- src: url("./fonts/materialdesignicons-webfont-4.9.95.eot");
- src: url("./fonts/materialdesignicons-webfont-4.9.95.woff2") format("woff2"),
- url("./fonts/materialdesignicons-webfont-4.9.95.woff") format("woff"),
- url("./fonts/materialdesignicons-webfont-4.9.95.ttf") format("truetype");
- font-weight: normal;
- font-style: normal;
-}
-.mdi:before,
-.mdi-set {
- display: inline-block;
- font: normal normal normal 24px/1 "Material Design Icons";
- font-size: inherit;
- text-rendering: auto;
- line-height: inherit;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-.mdi-ab-testing::before {
- content: "\F001C";
-}
-.mdi-abjad-arabic::before {
- content: "\F0353";
-}
-.mdi-abjad-hebrew::before {
- content: "\F0354";
-}
-.mdi-abugida-devanagari::before {
- content: "\F0355";
-}
-.mdi-abugida-thai::before {
- content: "\F0356";
-}
-.mdi-access-point::before {
- content: "\F002";
-}
-.mdi-access-point-network::before {
- content: "\F003";
-}
-.mdi-access-point-network-off::before {
- content: "\FBBD";
-}
-.mdi-account::before {
- content: "\F004";
-}
-.mdi-account-alert::before {
- content: "\F005";
-}
-.mdi-account-alert-outline::before {
- content: "\FB2C";
-}
-.mdi-account-arrow-left::before {
- content: "\FB2D";
-}
-.mdi-account-arrow-left-outline::before {
- content: "\FB2E";
-}
-.mdi-account-arrow-right::before {
- content: "\FB2F";
-}
-.mdi-account-arrow-right-outline::before {
- content: "\FB30";
-}
-.mdi-account-badge::before {
- content: "\FD83";
-}
-.mdi-account-badge-alert::before {
- content: "\FD84";
-}
-.mdi-account-badge-alert-outline::before {
- content: "\FD85";
-}
-.mdi-account-badge-horizontal::before {
- content: "\FDF0";
-}
-.mdi-account-badge-horizontal-outline::before {
- content: "\FDF1";
-}
-.mdi-account-badge-outline::before {
- content: "\FD86";
-}
-.mdi-account-box::before {
- content: "\F006";
-}
-.mdi-account-box-multiple::before {
- content: "\F933";
-}
-.mdi-account-box-multiple-outline::before {
- content: "\F002C";
-}
-.mdi-account-box-outline::before {
- content: "\F007";
-}
-.mdi-account-cancel::before {
- content: "\F030A";
-}
-.mdi-account-cancel-outline::before {
- content: "\F030B";
-}
-.mdi-account-card-details::before {
- content: "\F5D2";
-}
-.mdi-account-card-details-outline::before {
- content: "\FD87";
-}
-.mdi-account-cash::before {
- content: "\F00C2";
-}
-.mdi-account-cash-outline::before {
- content: "\F00C3";
-}
-.mdi-account-check::before {
- content: "\F008";
-}
-.mdi-account-check-outline::before {
- content: "\FBBE";
-}
-.mdi-account-child::before {
- content: "\FA88";
-}
-.mdi-account-child-circle::before {
- content: "\FA89";
-}
-.mdi-account-child-outline::before {
- content: "\F00F3";
-}
-.mdi-account-circle::before {
- content: "\F009";
-}
-.mdi-account-circle-outline::before {
- content: "\FB31";
-}
-.mdi-account-clock::before {
- content: "\FB32";
-}
-.mdi-account-clock-outline::before {
- content: "\FB33";
-}
-.mdi-account-cog::before {
- content: "\F039B";
-}
-.mdi-account-cog-outline::before {
- content: "\F039C";
-}
-.mdi-account-convert::before {
- content: "\F00A";
-}
-.mdi-account-convert-outline::before {
- content: "\F032C";
-}
-.mdi-account-details::before {
- content: "\F631";
-}
-.mdi-account-details-outline::before {
- content: "\F039D";
-}
-.mdi-account-edit::before {
- content: "\F6BB";
-}
-.mdi-account-edit-outline::before {
- content: "\F001D";
-}
-.mdi-account-group::before {
- content: "\F848";
-}
-.mdi-account-group-outline::before {
- content: "\FB34";
-}
-.mdi-account-heart::before {
- content: "\F898";
-}
-.mdi-account-heart-outline::before {
- content: "\FBBF";
-}
-.mdi-account-key::before {
- content: "\F00B";
-}
-.mdi-account-key-outline::before {
- content: "\FBC0";
-}
-.mdi-account-lock::before {
- content: "\F0189";
-}
-.mdi-account-lock-outline::before {
- content: "\F018A";
-}
-.mdi-account-minus::before {
- content: "\F00D";
-}
-.mdi-account-minus-outline::before {
- content: "\FAEB";
-}
-.mdi-account-multiple::before {
- content: "\F00E";
-}
-.mdi-account-multiple-check::before {
- content: "\F8C4";
-}
-.mdi-account-multiple-check-outline::before {
- content: "\F0229";
-}
-.mdi-account-multiple-minus::before {
- content: "\F5D3";
-}
-.mdi-account-multiple-minus-outline::before {
- content: "\FBC1";
-}
-.mdi-account-multiple-outline::before {
- content: "\F00F";
-}
-.mdi-account-multiple-plus::before {
- content: "\F010";
-}
-.mdi-account-multiple-plus-outline::before {
- content: "\F7FF";
-}
-.mdi-account-multiple-remove::before {
- content: "\F0235";
-}
-.mdi-account-multiple-remove-outline::before {
- content: "\F0236";
-}
-.mdi-account-network::before {
- content: "\F011";
-}
-.mdi-account-network-outline::before {
- content: "\FBC2";
-}
-.mdi-account-off::before {
- content: "\F012";
-}
-.mdi-account-off-outline::before {
- content: "\FBC3";
-}
-.mdi-account-outline::before {
- content: "\F013";
-}
-.mdi-account-plus::before {
- content: "\F014";
-}
-.mdi-account-plus-outline::before {
- content: "\F800";
-}
-.mdi-account-question::before {
- content: "\FB35";
-}
-.mdi-account-question-outline::before {
- content: "\FB36";
-}
-.mdi-account-remove::before {
- content: "\F015";
-}
-.mdi-account-remove-outline::before {
- content: "\FAEC";
-}
-.mdi-account-search::before {
- content: "\F016";
-}
-.mdi-account-search-outline::before {
- content: "\F934";
-}
-.mdi-account-settings::before {
- content: "\F630";
-}
-.mdi-account-settings-outline::before {
- content: "\F00F4";
-}
-.mdi-account-star::before {
- content: "\F017";
-}
-.mdi-account-star-outline::before {
- content: "\FBC4";
-}
-.mdi-account-supervisor::before {
- content: "\FA8A";
-}
-.mdi-account-supervisor-circle::before {
- content: "\FA8B";
-}
-.mdi-account-supervisor-outline::before {
- content: "\F0158";
-}
-.mdi-account-switch::before {
- content: "\F019";
-}
-.mdi-account-tie::before {
- content: "\FCBF";
-}
-.mdi-account-tie-outline::before {
- content: "\F00F5";
-}
-.mdi-account-tie-voice::before {
- content: "\F0333";
-}
-.mdi-account-tie-voice-off::before {
- content: "\F0335";
-}
-.mdi-account-tie-voice-off-outline::before {
- content: "\F0336";
-}
-.mdi-account-tie-voice-outline::before {
- content: "\F0334";
-}
-.mdi-accusoft::before {
- content: "\F849";
-}
-.mdi-adjust::before {
- content: "\F01A";
-}
-.mdi-adobe::before {
- content: "\F935";
-}
-.mdi-adobe-acrobat::before {
- content: "\FFBD";
-}
-.mdi-air-conditioner::before {
- content: "\F01B";
-}
-.mdi-air-filter::before {
- content: "\FD1F";
-}
-.mdi-air-horn::before {
- content: "\FD88";
-}
-.mdi-air-humidifier::before {
- content: "\F00C4";
-}
-.mdi-air-purifier::before {
- content: "\FD20";
-}
-.mdi-airbag::before {
- content: "\FBC5";
-}
-.mdi-airballoon::before {
- content: "\F01C";
-}
-.mdi-airballoon-outline::before {
- content: "\F002D";
-}
-.mdi-airplane::before {
- content: "\F01D";
-}
-.mdi-airplane-landing::before {
- content: "\F5D4";
-}
-.mdi-airplane-off::before {
- content: "\F01E";
-}
-.mdi-airplane-takeoff::before {
- content: "\F5D5";
-}
-.mdi-airplay::before {
- content: "\F01F";
-}
-.mdi-airport::before {
- content: "\F84A";
-}
-.mdi-alarm::before {
- content: "\F020";
-}
-.mdi-alarm-bell::before {
- content: "\F78D";
-}
-.mdi-alarm-check::before {
- content: "\F021";
-}
-.mdi-alarm-light::before {
- content: "\F78E";
-}
-.mdi-alarm-light-outline::before {
- content: "\FBC6";
-}
-.mdi-alarm-multiple::before {
- content: "\F022";
-}
-.mdi-alarm-note::before {
- content: "\FE8E";
-}
-.mdi-alarm-note-off::before {
- content: "\FE8F";
-}
-.mdi-alarm-off::before {
- content: "\F023";
-}
-.mdi-alarm-plus::before {
- content: "\F024";
-}
-.mdi-alarm-snooze::before {
- content: "\F68D";
-}
-.mdi-album::before {
- content: "\F025";
-}
-.mdi-alert::before {
- content: "\F026";
-}
-.mdi-alert-box::before {
- content: "\F027";
-}
-.mdi-alert-box-outline::before {
- content: "\FCC0";
-}
-.mdi-alert-circle::before {
- content: "\F028";
-}
-.mdi-alert-circle-check::before {
- content: "\F0218";
-}
-.mdi-alert-circle-check-outline::before {
- content: "\F0219";
-}
-.mdi-alert-circle-outline::before {
- content: "\F5D6";
-}
-.mdi-alert-decagram::before {
- content: "\F6BC";
-}
-.mdi-alert-decagram-outline::before {
- content: "\FCC1";
-}
-.mdi-alert-octagon::before {
- content: "\F029";
-}
-.mdi-alert-octagon-outline::before {
- content: "\FCC2";
-}
-.mdi-alert-octagram::before {
- content: "\F766";
-}
-.mdi-alert-octagram-outline::before {
- content: "\FCC3";
-}
-.mdi-alert-outline::before {
- content: "\F02A";
-}
-.mdi-alert-rhombus::before {
- content: "\F01F9";
-}
-.mdi-alert-rhombus-outline::before {
- content: "\F01FA";
-}
-.mdi-alien::before {
- content: "\F899";
-}
-.mdi-alien-outline::before {
- content: "\F00F6";
-}
-.mdi-align-horizontal-center::before {
- content: "\F01EE";
-}
-.mdi-align-horizontal-left::before {
- content: "\F01ED";
-}
-.mdi-align-horizontal-right::before {
- content: "\F01EF";
-}
-.mdi-align-vertical-bottom::before {
- content: "\F01F0";
-}
-.mdi-align-vertical-center::before {
- content: "\F01F1";
-}
-.mdi-align-vertical-top::before {
- content: "\F01F2";
-}
-.mdi-all-inclusive::before {
- content: "\F6BD";
-}
-.mdi-allergy::before {
- content: "\F0283";
-}
-.mdi-alpha::before {
- content: "\F02B";
-}
-.mdi-alpha-a::before {
- content: "\41";
-}
-.mdi-alpha-a-box::before {
- content: "\FAED";
-}
-.mdi-alpha-a-box-outline::before {
- content: "\FBC7";
-}
-.mdi-alpha-a-circle::before {
- content: "\FBC8";
-}
-.mdi-alpha-a-circle-outline::before {
- content: "\FBC9";
-}
-.mdi-alpha-b::before {
- content: "\42";
-}
-.mdi-alpha-b-box::before {
- content: "\FAEE";
-}
-.mdi-alpha-b-box-outline::before {
- content: "\FBCA";
-}
-.mdi-alpha-b-circle::before {
- content: "\FBCB";
-}
-.mdi-alpha-b-circle-outline::before {
- content: "\FBCC";
-}
-.mdi-alpha-c::before {
- content: "\43";
-}
-.mdi-alpha-c-box::before {
- content: "\FAEF";
-}
-.mdi-alpha-c-box-outline::before {
- content: "\FBCD";
-}
-.mdi-alpha-c-circle::before {
- content: "\FBCE";
-}
-.mdi-alpha-c-circle-outline::before {
- content: "\FBCF";
-}
-.mdi-alpha-d::before {
- content: "\44";
-}
-.mdi-alpha-d-box::before {
- content: "\FAF0";
-}
-.mdi-alpha-d-box-outline::before {
- content: "\FBD0";
-}
-.mdi-alpha-d-circle::before {
- content: "\FBD1";
-}
-.mdi-alpha-d-circle-outline::before {
- content: "\FBD2";
-}
-.mdi-alpha-e::before {
- content: "\45";
-}
-.mdi-alpha-e-box::before {
- content: "\FAF1";
-}
-.mdi-alpha-e-box-outline::before {
- content: "\FBD3";
-}
-.mdi-alpha-e-circle::before {
- content: "\FBD4";
-}
-.mdi-alpha-e-circle-outline::before {
- content: "\FBD5";
-}
-.mdi-alpha-f::before {
- content: "\46";
-}
-.mdi-alpha-f-box::before {
- content: "\FAF2";
-}
-.mdi-alpha-f-box-outline::before {
- content: "\FBD6";
-}
-.mdi-alpha-f-circle::before {
- content: "\FBD7";
-}
-.mdi-alpha-f-circle-outline::before {
- content: "\FBD8";
-}
-.mdi-alpha-g::before {
- content: "\47";
-}
-.mdi-alpha-g-box::before {
- content: "\FAF3";
-}
-.mdi-alpha-g-box-outline::before {
- content: "\FBD9";
-}
-.mdi-alpha-g-circle::before {
- content: "\FBDA";
-}
-.mdi-alpha-g-circle-outline::before {
- content: "\FBDB";
-}
-.mdi-alpha-h::before {
- content: "\48";
-}
-.mdi-alpha-h-box::before {
- content: "\FAF4";
-}
-.mdi-alpha-h-box-outline::before {
- content: "\FBDC";
-}
-.mdi-alpha-h-circle::before {
- content: "\FBDD";
-}
-.mdi-alpha-h-circle-outline::before {
- content: "\FBDE";
-}
-.mdi-alpha-i::before {
- content: "\49";
-}
-.mdi-alpha-i-box::before {
- content: "\FAF5";
-}
-.mdi-alpha-i-box-outline::before {
- content: "\FBDF";
-}
-.mdi-alpha-i-circle::before {
- content: "\FBE0";
-}
-.mdi-alpha-i-circle-outline::before {
- content: "\FBE1";
-}
-.mdi-alpha-j::before {
- content: "\4A";
-}
-.mdi-alpha-j-box::before {
- content: "\FAF6";
-}
-.mdi-alpha-j-box-outline::before {
- content: "\FBE2";
-}
-.mdi-alpha-j-circle::before {
- content: "\FBE3";
-}
-.mdi-alpha-j-circle-outline::before {
- content: "\FBE4";
-}
-.mdi-alpha-k::before {
- content: "\4B";
-}
-.mdi-alpha-k-box::before {
- content: "\FAF7";
-}
-.mdi-alpha-k-box-outline::before {
- content: "\FBE5";
-}
-.mdi-alpha-k-circle::before {
- content: "\FBE6";
-}
-.mdi-alpha-k-circle-outline::before {
- content: "\FBE7";
-}
-.mdi-alpha-l::before {
- content: "\4C";
-}
-.mdi-alpha-l-box::before {
- content: "\FAF8";
-}
-.mdi-alpha-l-box-outline::before {
- content: "\FBE8";
-}
-.mdi-alpha-l-circle::before {
- content: "\FBE9";
-}
-.mdi-alpha-l-circle-outline::before {
- content: "\FBEA";
-}
-.mdi-alpha-m::before {
- content: "\4D";
-}
-.mdi-alpha-m-box::before {
- content: "\FAF9";
-}
-.mdi-alpha-m-box-outline::before {
- content: "\FBEB";
-}
-.mdi-alpha-m-circle::before {
- content: "\FBEC";
-}
-.mdi-alpha-m-circle-outline::before {
- content: "\FBED";
-}
-.mdi-alpha-n::before {
- content: "\4E";
-}
-.mdi-alpha-n-box::before {
- content: "\FAFA";
-}
-.mdi-alpha-n-box-outline::before {
- content: "\FBEE";
-}
-.mdi-alpha-n-circle::before {
- content: "\FBEF";
-}
-.mdi-alpha-n-circle-outline::before {
- content: "\FBF0";
-}
-.mdi-alpha-o::before {
- content: "\4F";
-}
-.mdi-alpha-o-box::before {
- content: "\FAFB";
-}
-.mdi-alpha-o-box-outline::before {
- content: "\FBF1";
-}
-.mdi-alpha-o-circle::before {
- content: "\FBF2";
-}
-.mdi-alpha-o-circle-outline::before {
- content: "\FBF3";
-}
-.mdi-alpha-p::before {
- content: "\50";
-}
-.mdi-alpha-p-box::before {
- content: "\FAFC";
-}
-.mdi-alpha-p-box-outline::before {
- content: "\FBF4";
-}
-.mdi-alpha-p-circle::before {
- content: "\FBF5";
-}
-.mdi-alpha-p-circle-outline::before {
- content: "\FBF6";
-}
-.mdi-alpha-q::before {
- content: "\51";
-}
-.mdi-alpha-q-box::before {
- content: "\FAFD";
-}
-.mdi-alpha-q-box-outline::before {
- content: "\FBF7";
-}
-.mdi-alpha-q-circle::before {
- content: "\FBF8";
-}
-.mdi-alpha-q-circle-outline::before {
- content: "\FBF9";
-}
-.mdi-alpha-r::before {
- content: "\52";
-}
-.mdi-alpha-r-box::before {
- content: "\FAFE";
-}
-.mdi-alpha-r-box-outline::before {
- content: "\FBFA";
-}
-.mdi-alpha-r-circle::before {
- content: "\FBFB";
-}
-.mdi-alpha-r-circle-outline::before {
- content: "\FBFC";
-}
-.mdi-alpha-s::before {
- content: "\53";
-}
-.mdi-alpha-s-box::before {
- content: "\FAFF";
-}
-.mdi-alpha-s-box-outline::before {
- content: "\FBFD";
-}
-.mdi-alpha-s-circle::before {
- content: "\FBFE";
-}
-.mdi-alpha-s-circle-outline::before {
- content: "\FBFF";
-}
-.mdi-alpha-t::before {
- content: "\54";
-}
-.mdi-alpha-t-box::before {
- content: "\FB00";
-}
-.mdi-alpha-t-box-outline::before {
- content: "\FC00";
-}
-.mdi-alpha-t-circle::before {
- content: "\FC01";
-}
-.mdi-alpha-t-circle-outline::before {
- content: "\FC02";
-}
-.mdi-alpha-u::before {
- content: "\55";
-}
-.mdi-alpha-u-box::before {
- content: "\FB01";
-}
-.mdi-alpha-u-box-outline::before {
- content: "\FC03";
-}
-.mdi-alpha-u-circle::before {
- content: "\FC04";
-}
-.mdi-alpha-u-circle-outline::before {
- content: "\FC05";
-}
-.mdi-alpha-v::before {
- content: "\56";
-}
-.mdi-alpha-v-box::before {
- content: "\FB02";
-}
-.mdi-alpha-v-box-outline::before {
- content: "\FC06";
-}
-.mdi-alpha-v-circle::before {
- content: "\FC07";
-}
-.mdi-alpha-v-circle-outline::before {
- content: "\FC08";
-}
-.mdi-alpha-w::before {
- content: "\57";
-}
-.mdi-alpha-w-box::before {
- content: "\FB03";
-}
-.mdi-alpha-w-box-outline::before {
- content: "\FC09";
-}
-.mdi-alpha-w-circle::before {
- content: "\FC0A";
-}
-.mdi-alpha-w-circle-outline::before {
- content: "\FC0B";
-}
-.mdi-alpha-x::before {
- content: "\58";
-}
-.mdi-alpha-x-box::before {
- content: "\FB04";
-}
-.mdi-alpha-x-box-outline::before {
- content: "\FC0C";
-}
-.mdi-alpha-x-circle::before {
- content: "\FC0D";
-}
-.mdi-alpha-x-circle-outline::before {
- content: "\FC0E";
-}
-.mdi-alpha-y::before {
- content: "\59";
-}
-.mdi-alpha-y-box::before {
- content: "\FB05";
-}
-.mdi-alpha-y-box-outline::before {
- content: "\FC0F";
-}
-.mdi-alpha-y-circle::before {
- content: "\FC10";
-}
-.mdi-alpha-y-circle-outline::before {
- content: "\FC11";
-}
-.mdi-alpha-z::before {
- content: "\5A";
-}
-.mdi-alpha-z-box::before {
- content: "\FB06";
-}
-.mdi-alpha-z-box-outline::before {
- content: "\FC12";
-}
-.mdi-alpha-z-circle::before {
- content: "\FC13";
-}
-.mdi-alpha-z-circle-outline::before {
- content: "\FC14";
-}
-.mdi-alphabet-aurebesh::before {
- content: "\F0357";
-}
-.mdi-alphabet-cyrillic::before {
- content: "\F0358";
-}
-.mdi-alphabet-greek::before {
- content: "\F0359";
-}
-.mdi-alphabet-latin::before {
- content: "\F035A";
-}
-.mdi-alphabet-piqad::before {
- content: "\F035B";
-}
-.mdi-alphabet-tengwar::before {
- content: "\F0362";
-}
-.mdi-alphabetical::before {
- content: "\F02C";
-}
-.mdi-alphabetical-off::before {
- content: "\F002E";
-}
-.mdi-alphabetical-variant::before {
- content: "\F002F";
-}
-.mdi-alphabetical-variant-off::before {
- content: "\F0030";
-}
-.mdi-altimeter::before {
- content: "\F5D7";
-}
-.mdi-amazon::before {
- content: "\F02D";
-}
-.mdi-amazon-alexa::before {
- content: "\F8C5";
-}
-.mdi-amazon-drive::before {
- content: "\F02E";
-}
-.mdi-ambulance::before {
- content: "\F02F";
-}
-.mdi-ammunition::before {
- content: "\FCC4";
-}
-.mdi-ampersand::before {
- content: "\FA8C";
-}
-.mdi-amplifier::before {
- content: "\F030";
-}
-.mdi-amplifier-off::before {
- content: "\F01E0";
-}
-.mdi-anchor::before {
- content: "\F031";
-}
-.mdi-android::before {
- content: "\F032";
-}
-.mdi-android-auto::before {
- content: "\FA8D";
-}
-.mdi-android-debug-bridge::before {
- content: "\F033";
-}
-.mdi-android-head::before {
- content: "\F78F";
-}
-.mdi-android-messages::before {
- content: "\FD21";
-}
-.mdi-android-studio::before {
- content: "\F034";
-}
-.mdi-angle-acute::before {
- content: "\F936";
-}
-.mdi-angle-obtuse::before {
- content: "\F937";
-}
-.mdi-angle-right::before {
- content: "\F938";
-}
-.mdi-angular::before {
- content: "\F6B1";
-}
-.mdi-angularjs::before {
- content: "\F6BE";
-}
-.mdi-animation::before {
- content: "\F5D8";
-}
-.mdi-animation-outline::before {
- content: "\FA8E";
-}
-.mdi-animation-play::before {
- content: "\F939";
-}
-.mdi-animation-play-outline::before {
- content: "\FA8F";
-}
-.mdi-ansible::before {
- content: "\F00C5";
-}
-.mdi-antenna::before {
- content: "\F0144";
-}
-.mdi-anvil::before {
- content: "\F89A";
-}
-.mdi-apache-kafka::before {
- content: "\F0031";
-}
-.mdi-api::before {
- content: "\F00C6";
-}
-.mdi-api-off::before {
- content: "\F0282";
-}
-.mdi-apple::before {
- content: "\F035";
-}
-.mdi-apple-finder::before {
- content: "\F036";
-}
-.mdi-apple-icloud::before {
- content: "\F038";
-}
-.mdi-apple-ios::before {
- content: "\F037";
-}
-.mdi-apple-keyboard-caps::before {
- content: "\F632";
-}
-.mdi-apple-keyboard-command::before {
- content: "\F633";
-}
-.mdi-apple-keyboard-control::before {
- content: "\F634";
-}
-.mdi-apple-keyboard-option::before {
- content: "\F635";
-}
-.mdi-apple-keyboard-shift::before {
- content: "\F636";
-}
-.mdi-apple-safari::before {
- content: "\F039";
-}
-.mdi-application::before {
- content: "\F614";
-}
-.mdi-application-export::before {
- content: "\FD89";
-}
-.mdi-application-import::before {
- content: "\FD8A";
-}
-.mdi-approximately-equal::before {
- content: "\FFBE";
-}
-.mdi-approximately-equal-box::before {
- content: "\FFBF";
-}
-.mdi-apps::before {
- content: "\F03B";
-}
-.mdi-apps-box::before {
- content: "\FD22";
-}
-.mdi-arch::before {
- content: "\F8C6";
-}
-.mdi-archive::before {
- content: "\F03C";
-}
-.mdi-archive-arrow-down::before {
- content: "\F0284";
-}
-.mdi-archive-arrow-down-outline::before {
- content: "\F0285";
-}
-.mdi-archive-arrow-up::before {
- content: "\F0286";
-}
-.mdi-archive-arrow-up-outline::before {
- content: "\F0287";
-}
-.mdi-archive-outline::before {
- content: "\F0239";
-}
-.mdi-arm-flex::before {
- content: "\F008F";
-}
-.mdi-arm-flex-outline::before {
- content: "\F0090";
-}
-.mdi-arrange-bring-forward::before {
- content: "\F03D";
-}
-.mdi-arrange-bring-to-front::before {
- content: "\F03E";
-}
-.mdi-arrange-send-backward::before {
- content: "\F03F";
-}
-.mdi-arrange-send-to-back::before {
- content: "\F040";
-}
-.mdi-arrow-all::before {
- content: "\F041";
-}
-.mdi-arrow-bottom-left::before {
- content: "\F042";
-}
-.mdi-arrow-bottom-left-bold-outline::before {
- content: "\F9B6";
-}
-.mdi-arrow-bottom-left-thick::before {
- content: "\F9B7";
-}
-.mdi-arrow-bottom-right::before {
- content: "\F043";
-}
-.mdi-arrow-bottom-right-bold-outline::before {
- content: "\F9B8";
-}
-.mdi-arrow-bottom-right-thick::before {
- content: "\F9B9";
-}
-.mdi-arrow-collapse::before {
- content: "\F615";
-}
-.mdi-arrow-collapse-all::before {
- content: "\F044";
-}
-.mdi-arrow-collapse-down::before {
- content: "\F791";
-}
-.mdi-arrow-collapse-horizontal::before {
- content: "\F84B";
-}
-.mdi-arrow-collapse-left::before {
- content: "\F792";
-}
-.mdi-arrow-collapse-right::before {
- content: "\F793";
-}
-.mdi-arrow-collapse-up::before {
- content: "\F794";
-}
-.mdi-arrow-collapse-vertical::before {
- content: "\F84C";
-}
-.mdi-arrow-decision::before {
- content: "\F9BA";
-}
-.mdi-arrow-decision-auto::before {
- content: "\F9BB";
-}
-.mdi-arrow-decision-auto-outline::before {
- content: "\F9BC";
-}
-.mdi-arrow-decision-outline::before {
- content: "\F9BD";
-}
-.mdi-arrow-down::before {
- content: "\F045";
-}
-.mdi-arrow-down-bold::before {
- content: "\F72D";
-}
-.mdi-arrow-down-bold-box::before {
- content: "\F72E";
-}
-.mdi-arrow-down-bold-box-outline::before {
- content: "\F72F";
-}
-.mdi-arrow-down-bold-circle::before {
- content: "\F047";
-}
-.mdi-arrow-down-bold-circle-outline::before {
- content: "\F048";
-}
-.mdi-arrow-down-bold-hexagon-outline::before {
- content: "\F049";
-}
-.mdi-arrow-down-bold-outline::before {
- content: "\F9BE";
-}
-.mdi-arrow-down-box::before {
- content: "\F6BF";
-}
-.mdi-arrow-down-circle::before {
- content: "\FCB7";
-}
-.mdi-arrow-down-circle-outline::before {
- content: "\FCB8";
-}
-.mdi-arrow-down-drop-circle::before {
- content: "\F04A";
-}
-.mdi-arrow-down-drop-circle-outline::before {
- content: "\F04B";
-}
-.mdi-arrow-down-thick::before {
- content: "\F046";
-}
-.mdi-arrow-expand::before {
- content: "\F616";
-}
-.mdi-arrow-expand-all::before {
- content: "\F04C";
-}
-.mdi-arrow-expand-down::before {
- content: "\F795";
-}
-.mdi-arrow-expand-horizontal::before {
- content: "\F84D";
-}
-.mdi-arrow-expand-left::before {
- content: "\F796";
-}
-.mdi-arrow-expand-right::before {
- content: "\F797";
-}
-.mdi-arrow-expand-up::before {
- content: "\F798";
-}
-.mdi-arrow-expand-vertical::before {
- content: "\F84E";
-}
-.mdi-arrow-horizontal-lock::before {
- content: "\F0186";
-}
-.mdi-arrow-left::before {
- content: "\F04D";
-}
-.mdi-arrow-left-bold::before {
- content: "\F730";
-}
-.mdi-arrow-left-bold-box::before {
- content: "\F731";
-}
-.mdi-arrow-left-bold-box-outline::before {
- content: "\F732";
-}
-.mdi-arrow-left-bold-circle::before {
- content: "\F04F";
-}
-.mdi-arrow-left-bold-circle-outline::before {
- content: "\F050";
-}
-.mdi-arrow-left-bold-hexagon-outline::before {
- content: "\F051";
-}
-.mdi-arrow-left-bold-outline::before {
- content: "\F9BF";
-}
-.mdi-arrow-left-box::before {
- content: "\F6C0";
-}
-.mdi-arrow-left-circle::before {
- content: "\FCB9";
-}
-.mdi-arrow-left-circle-outline::before {
- content: "\FCBA";
-}
-.mdi-arrow-left-drop-circle::before {
- content: "\F052";
-}
-.mdi-arrow-left-drop-circle-outline::before {
- content: "\F053";
-}
-.mdi-arrow-left-right::before {
- content: "\FE90";
-}
-.mdi-arrow-left-right-bold::before {
- content: "\FE91";
-}
-.mdi-arrow-left-right-bold-outline::before {
- content: "\F9C0";
-}
-.mdi-arrow-left-thick::before {
- content: "\F04E";
-}
-.mdi-arrow-right::before {
- content: "\F054";
-}
-.mdi-arrow-right-bold::before {
- content: "\F733";
-}
-.mdi-arrow-right-bold-box::before {
- content: "\F734";
-}
-.mdi-arrow-right-bold-box-outline::before {
- content: "\F735";
-}
-.mdi-arrow-right-bold-circle::before {
- content: "\F056";
-}
-.mdi-arrow-right-bold-circle-outline::before {
- content: "\F057";
-}
-.mdi-arrow-right-bold-hexagon-outline::before {
- content: "\F058";
-}
-.mdi-arrow-right-bold-outline::before {
- content: "\F9C1";
-}
-.mdi-arrow-right-box::before {
- content: "\F6C1";
-}
-.mdi-arrow-right-circle::before {
- content: "\FCBB";
-}
-.mdi-arrow-right-circle-outline::before {
- content: "\FCBC";
-}
-.mdi-arrow-right-drop-circle::before {
- content: "\F059";
-}
-.mdi-arrow-right-drop-circle-outline::before {
- content: "\F05A";
-}
-.mdi-arrow-right-thick::before {
- content: "\F055";
-}
-.mdi-arrow-split-horizontal::before {
- content: "\F93A";
-}
-.mdi-arrow-split-vertical::before {
- content: "\F93B";
-}
-.mdi-arrow-top-left::before {
- content: "\F05B";
-}
-.mdi-arrow-top-left-bold-outline::before {
- content: "\F9C2";
-}
-.mdi-arrow-top-left-bottom-right::before {
- content: "\FE92";
-}
-.mdi-arrow-top-left-bottom-right-bold::before {
- content: "\FE93";
-}
-.mdi-arrow-top-left-thick::before {
- content: "\F9C3";
-}
-.mdi-arrow-top-right::before {
- content: "\F05C";
-}
-.mdi-arrow-top-right-bold-outline::before {
- content: "\F9C4";
-}
-.mdi-arrow-top-right-bottom-left::before {
- content: "\FE94";
-}
-.mdi-arrow-top-right-bottom-left-bold::before {
- content: "\FE95";
-}
-.mdi-arrow-top-right-thick::before {
- content: "\F9C5";
-}
-.mdi-arrow-up::before {
- content: "\F05D";
-}
-.mdi-arrow-up-bold::before {
- content: "\F736";
-}
-.mdi-arrow-up-bold-box::before {
- content: "\F737";
-}
-.mdi-arrow-up-bold-box-outline::before {
- content: "\F738";
-}
-.mdi-arrow-up-bold-circle::before {
- content: "\F05F";
-}
-.mdi-arrow-up-bold-circle-outline::before {
- content: "\F060";
-}
-.mdi-arrow-up-bold-hexagon-outline::before {
- content: "\F061";
-}
-.mdi-arrow-up-bold-outline::before {
- content: "\F9C6";
-}
-.mdi-arrow-up-box::before {
- content: "\F6C2";
-}
-.mdi-arrow-up-circle::before {
- content: "\FCBD";
-}
-.mdi-arrow-up-circle-outline::before {
- content: "\FCBE";
-}
-.mdi-arrow-up-down::before {
- content: "\FE96";
-}
-.mdi-arrow-up-down-bold::before {
- content: "\FE97";
-}
-.mdi-arrow-up-down-bold-outline::before {
- content: "\F9C7";
-}
-.mdi-arrow-up-drop-circle::before {
- content: "\F062";
-}
-.mdi-arrow-up-drop-circle-outline::before {
- content: "\F063";
-}
-.mdi-arrow-up-thick::before {
- content: "\F05E";
-}
-.mdi-arrow-vertical-lock::before {
- content: "\F0187";
-}
-.mdi-artist::before {
- content: "\F802";
-}
-.mdi-artist-outline::before {
- content: "\FCC5";
-}
-.mdi-artstation::before {
- content: "\FB37";
-}
-.mdi-aspect-ratio::before {
- content: "\FA23";
-}
-.mdi-assistant::before {
- content: "\F064";
-}
-.mdi-asterisk::before {
- content: "\F6C3";
-}
-.mdi-at::before {
- content: "\F065";
-}
-.mdi-atlassian::before {
- content: "\F803";
-}
-.mdi-atm::before {
- content: "\FD23";
-}
-.mdi-atom::before {
- content: "\F767";
-}
-.mdi-atom-variant::before {
- content: "\FE98";
-}
-.mdi-attachment::before {
- content: "\F066";
-}
-.mdi-audio-video::before {
- content: "\F93C";
-}
-.mdi-audio-video-off::before {
- content: "\F01E1";
-}
-.mdi-audiobook::before {
- content: "\F067";
-}
-.mdi-augmented-reality::before {
- content: "\F84F";
-}
-.mdi-auto-download::before {
- content: "\F03A9";
-}
-.mdi-auto-fix::before {
- content: "\F068";
-}
-.mdi-auto-upload::before {
- content: "\F069";
-}
-.mdi-autorenew::before {
- content: "\F06A";
-}
-.mdi-av-timer::before {
- content: "\F06B";
-}
-.mdi-aws::before {
- content: "\FDF2";
-}
-.mdi-axe::before {
- content: "\F8C7";
-}
-.mdi-axis::before {
- content: "\FD24";
-}
-.mdi-axis-arrow::before {
- content: "\FD25";
-}
-.mdi-axis-arrow-lock::before {
- content: "\FD26";
-}
-.mdi-axis-lock::before {
- content: "\FD27";
-}
-.mdi-axis-x-arrow::before {
- content: "\FD28";
-}
-.mdi-axis-x-arrow-lock::before {
- content: "\FD29";
-}
-.mdi-axis-x-rotate-clockwise::before {
- content: "\FD2A";
-}
-.mdi-axis-x-rotate-counterclockwise::before {
- content: "\FD2B";
-}
-.mdi-axis-x-y-arrow-lock::before {
- content: "\FD2C";
-}
-.mdi-axis-y-arrow::before {
- content: "\FD2D";
-}
-.mdi-axis-y-arrow-lock::before {
- content: "\FD2E";
-}
-.mdi-axis-y-rotate-clockwise::before {
- content: "\FD2F";
-}
-.mdi-axis-y-rotate-counterclockwise::before {
- content: "\FD30";
-}
-.mdi-axis-z-arrow::before {
- content: "\FD31";
-}
-.mdi-axis-z-arrow-lock::before {
- content: "\FD32";
-}
-.mdi-axis-z-rotate-clockwise::before {
- content: "\FD33";
-}
-.mdi-axis-z-rotate-counterclockwise::before {
- content: "\FD34";
-}
-.mdi-azure::before {
- content: "\F804";
-}
-.mdi-azure-devops::before {
- content: "\F0091";
-}
-.mdi-babel::before {
- content: "\FA24";
-}
-.mdi-baby::before {
- content: "\F06C";
-}
-.mdi-baby-bottle::before {
- content: "\FF56";
-}
-.mdi-baby-bottle-outline::before {
- content: "\FF57";
-}
-.mdi-baby-carriage::before {
- content: "\F68E";
-}
-.mdi-baby-carriage-off::before {
- content: "\FFC0";
-}
-.mdi-baby-face::before {
- content: "\FE99";
-}
-.mdi-baby-face-outline::before {
- content: "\FE9A";
-}
-.mdi-backburger::before {
- content: "\F06D";
-}
-.mdi-backspace::before {
- content: "\F06E";
-}
-.mdi-backspace-outline::before {
- content: "\FB38";
-}
-.mdi-backspace-reverse::before {
- content: "\FE9B";
-}
-.mdi-backspace-reverse-outline::before {
- content: "\FE9C";
-}
-.mdi-backup-restore::before {
- content: "\F06F";
-}
-.mdi-bacteria::before {
- content: "\FEF2";
-}
-.mdi-bacteria-outline::before {
- content: "\FEF3";
-}
-.mdi-badminton::before {
- content: "\F850";
-}
-.mdi-bag-carry-on::before {
- content: "\FF58";
-}
-.mdi-bag-carry-on-check::before {
- content: "\FD41";
-}
-.mdi-bag-carry-on-off::before {
- content: "\FF59";
-}
-.mdi-bag-checked::before {
- content: "\FF5A";
-}
-.mdi-bag-personal::before {
- content: "\FDF3";
-}
-.mdi-bag-personal-off::before {
- content: "\FDF4";
-}
-.mdi-bag-personal-off-outline::before {
- content: "\FDF5";
-}
-.mdi-bag-personal-outline::before {
- content: "\FDF6";
-}
-.mdi-baguette::before {
- content: "\FF5B";
-}
-.mdi-balloon::before {
- content: "\FA25";
-}
-.mdi-ballot::before {
- content: "\F9C8";
-}
-.mdi-ballot-outline::before {
- content: "\F9C9";
-}
-.mdi-ballot-recount::before {
- content: "\FC15";
-}
-.mdi-ballot-recount-outline::before {
- content: "\FC16";
-}
-.mdi-bandage::before {
- content: "\FD8B";
-}
-.mdi-bandcamp::before {
- content: "\F674";
-}
-.mdi-bank::before {
- content: "\F070";
-}
-.mdi-bank-minus::before {
- content: "\FD8C";
-}
-.mdi-bank-outline::before {
- content: "\FE9D";
-}
-.mdi-bank-plus::before {
- content: "\FD8D";
-}
-.mdi-bank-remove::before {
- content: "\FD8E";
-}
-.mdi-bank-transfer::before {
- content: "\FA26";
-}
-.mdi-bank-transfer-in::before {
- content: "\FA27";
-}
-.mdi-bank-transfer-out::before {
- content: "\FA28";
-}
-.mdi-barcode::before {
- content: "\F071";
-}
-.mdi-barcode-off::before {
- content: "\F0261";
-}
-.mdi-barcode-scan::before {
- content: "\F072";
-}
-.mdi-barley::before {
- content: "\F073";
-}
-.mdi-barley-off::before {
- content: "\FB39";
-}
-.mdi-barn::before {
- content: "\FB3A";
-}
-.mdi-barrel::before {
- content: "\F074";
-}
-.mdi-baseball::before {
- content: "\F851";
-}
-.mdi-baseball-bat::before {
- content: "\F852";
-}
-.mdi-basecamp::before {
- content: "\F075";
-}
-.mdi-bash::before {
- content: "\F01AE";
-}
-.mdi-basket::before {
- content: "\F076";
-}
-.mdi-basket-fill::before {
- content: "\F077";
-}
-.mdi-basket-outline::before {
- content: "\F01AC";
-}
-.mdi-basket-unfill::before {
- content: "\F078";
-}
-.mdi-basketball::before {
- content: "\F805";
-}
-.mdi-basketball-hoop::before {
- content: "\FC17";
-}
-.mdi-basketball-hoop-outline::before {
- content: "\FC18";
-}
-.mdi-bat::before {
- content: "\FB3B";
-}
-.mdi-battery::before {
- content: "\F079";
-}
-.mdi-battery-10::before {
- content: "\F07A";
-}
-.mdi-battery-10-bluetooth::before {
- content: "\F93D";
-}
-.mdi-battery-20::before {
- content: "\F07B";
-}
-.mdi-battery-20-bluetooth::before {
- content: "\F93E";
-}
-.mdi-battery-30::before {
- content: "\F07C";
-}
-.mdi-battery-30-bluetooth::before {
- content: "\F93F";
-}
-.mdi-battery-40::before {
- content: "\F07D";
-}
-.mdi-battery-40-bluetooth::before {
- content: "\F940";
-}
-.mdi-battery-50::before {
- content: "\F07E";
-}
-.mdi-battery-50-bluetooth::before {
- content: "\F941";
-}
-.mdi-battery-60::before {
- content: "\F07F";
-}
-.mdi-battery-60-bluetooth::before {
- content: "\F942";
-}
-.mdi-battery-70::before {
- content: "\F080";
-}
-.mdi-battery-70-bluetooth::before {
- content: "\F943";
-}
-.mdi-battery-80::before {
- content: "\F081";
-}
-.mdi-battery-80-bluetooth::before {
- content: "\F944";
-}
-.mdi-battery-90::before {
- content: "\F082";
-}
-.mdi-battery-90-bluetooth::before {
- content: "\F945";
-}
-.mdi-battery-alert::before {
- content: "\F083";
-}
-.mdi-battery-alert-bluetooth::before {
- content: "\F946";
-}
-.mdi-battery-alert-variant::before {
- content: "\F00F7";
-}
-.mdi-battery-alert-variant-outline::before {
- content: "\F00F8";
-}
-.mdi-battery-bluetooth::before {
- content: "\F947";
-}
-.mdi-battery-bluetooth-variant::before {
- content: "\F948";
-}
-.mdi-battery-charging::before {
- content: "\F084";
-}
-.mdi-battery-charging-10::before {
- content: "\F89B";
-}
-.mdi-battery-charging-100::before {
- content: "\F085";
-}
-.mdi-battery-charging-20::before {
- content: "\F086";
-}
-.mdi-battery-charging-30::before {
- content: "\F087";
-}
-.mdi-battery-charging-40::before {
- content: "\F088";
-}
-.mdi-battery-charging-50::before {
- content: "\F89C";
-}
-.mdi-battery-charging-60::before {
- content: "\F089";
-}
-.mdi-battery-charging-70::before {
- content: "\F89D";
-}
-.mdi-battery-charging-80::before {
- content: "\F08A";
-}
-.mdi-battery-charging-90::before {
- content: "\F08B";
-}
-.mdi-battery-charging-high::before {
- content: "\F02D1";
-}
-.mdi-battery-charging-low::before {
- content: "\F02CF";
-}
-.mdi-battery-charging-medium::before {
- content: "\F02D0";
-}
-.mdi-battery-charging-outline::before {
- content: "\F89E";
-}
-.mdi-battery-charging-wireless::before {
- content: "\F806";
-}
-.mdi-battery-charging-wireless-10::before {
- content: "\F807";
-}
-.mdi-battery-charging-wireless-20::before {
- content: "\F808";
-}
-.mdi-battery-charging-wireless-30::before {
- content: "\F809";
-}
-.mdi-battery-charging-wireless-40::before {
- content: "\F80A";
-}
-.mdi-battery-charging-wireless-50::before {
- content: "\F80B";
-}
-.mdi-battery-charging-wireless-60::before {
- content: "\F80C";
-}
-.mdi-battery-charging-wireless-70::before {
- content: "\F80D";
-}
-.mdi-battery-charging-wireless-80::before {
- content: "\F80E";
-}
-.mdi-battery-charging-wireless-90::before {
- content: "\F80F";
-}
-.mdi-battery-charging-wireless-alert::before {
- content: "\F810";
-}
-.mdi-battery-charging-wireless-outline::before {
- content: "\F811";
-}
-.mdi-battery-heart::before {
- content: "\F023A";
-}
-.mdi-battery-heart-outline::before {
- content: "\F023B";
-}
-.mdi-battery-heart-variant::before {
- content: "\F023C";
-}
-.mdi-battery-high::before {
- content: "\F02CE";
-}
-.mdi-battery-low::before {
- content: "\F02CC";
-}
-.mdi-battery-medium::before {
- content: "\F02CD";
-}
-.mdi-battery-minus::before {
- content: "\F08C";
-}
-.mdi-battery-negative::before {
- content: "\F08D";
-}
-.mdi-battery-off::before {
- content: "\F0288";
-}
-.mdi-battery-off-outline::before {
- content: "\F0289";
-}
-.mdi-battery-outline::before {
- content: "\F08E";
-}
-.mdi-battery-plus::before {
- content: "\F08F";
-}
-.mdi-battery-positive::before {
- content: "\F090";
-}
-.mdi-battery-unknown::before {
- content: "\F091";
-}
-.mdi-battery-unknown-bluetooth::before {
- content: "\F949";
-}
-.mdi-battlenet::before {
- content: "\FB3C";
-}
-.mdi-beach::before {
- content: "\F092";
-}
-.mdi-beaker::before {
- content: "\FCC6";
-}
-.mdi-beaker-alert::before {
- content: "\F0254";
-}
-.mdi-beaker-alert-outline::before {
- content: "\F0255";
-}
-.mdi-beaker-check::before {
- content: "\F0256";
-}
-.mdi-beaker-check-outline::before {
- content: "\F0257";
-}
-.mdi-beaker-minus::before {
- content: "\F0258";
-}
-.mdi-beaker-minus-outline::before {
- content: "\F0259";
-}
-.mdi-beaker-outline::before {
- content: "\F68F";
-}
-.mdi-beaker-plus::before {
- content: "\F025A";
-}
-.mdi-beaker-plus-outline::before {
- content: "\F025B";
-}
-.mdi-beaker-question::before {
- content: "\F025C";
-}
-.mdi-beaker-question-outline::before {
- content: "\F025D";
-}
-.mdi-beaker-remove::before {
- content: "\F025E";
-}
-.mdi-beaker-remove-outline::before {
- content: "\F025F";
-}
-.mdi-beats::before {
- content: "\F097";
-}
-.mdi-bed-double::before {
- content: "\F0092";
-}
-.mdi-bed-double-outline::before {
- content: "\F0093";
-}
-.mdi-bed-empty::before {
- content: "\F89F";
-}
-.mdi-bed-king::before {
- content: "\F0094";
-}
-.mdi-bed-king-outline::before {
- content: "\F0095";
-}
-.mdi-bed-queen::before {
- content: "\F0096";
-}
-.mdi-bed-queen-outline::before {
- content: "\F0097";
-}
-.mdi-bed-single::before {
- content: "\F0098";
-}
-.mdi-bed-single-outline::before {
- content: "\F0099";
-}
-.mdi-bee::before {
- content: "\FFC1";
-}
-.mdi-bee-flower::before {
- content: "\FFC2";
-}
-.mdi-beehive-outline::before {
- content: "\F00F9";
-}
-.mdi-beer::before {
- content: "\F098";
-}
-.mdi-beer-outline::before {
- content: "\F0337";
-}
-.mdi-behance::before {
- content: "\F099";
-}
-.mdi-bell::before {
- content: "\F09A";
-}
-.mdi-bell-alert::before {
- content: "\FD35";
-}
-.mdi-bell-alert-outline::before {
- content: "\FE9E";
-}
-.mdi-bell-check::before {
- content: "\F0210";
-}
-.mdi-bell-check-outline::before {
- content: "\F0211";
-}
-.mdi-bell-circle::before {
- content: "\FD36";
-}
-.mdi-bell-circle-outline::before {
- content: "\FD37";
-}
-.mdi-bell-off::before {
- content: "\F09B";
-}
-.mdi-bell-off-outline::before {
- content: "\FA90";
-}
-.mdi-bell-outline::before {
- content: "\F09C";
-}
-.mdi-bell-plus::before {
- content: "\F09D";
-}
-.mdi-bell-plus-outline::before {
- content: "\FA91";
-}
-.mdi-bell-ring::before {
- content: "\F09E";
-}
-.mdi-bell-ring-outline::before {
- content: "\F09F";
-}
-.mdi-bell-sleep::before {
- content: "\F0A0";
-}
-.mdi-bell-sleep-outline::before {
- content: "\FA92";
-}
-.mdi-beta::before {
- content: "\F0A1";
-}
-.mdi-betamax::before {
- content: "\F9CA";
-}
-.mdi-biathlon::before {
- content: "\FDF7";
-}
-.mdi-bible::before {
- content: "\F0A2";
-}
-.mdi-bicycle::before {
- content: "\F00C7";
-}
-.mdi-bicycle-basket::before {
- content: "\F0260";
-}
-.mdi-bike::before {
- content: "\F0A3";
-}
-.mdi-bike-fast::before {
- content: "\F014A";
-}
-.mdi-billboard::before {
- content: "\F0032";
-}
-.mdi-billiards::before {
- content: "\FB3D";
-}
-.mdi-billiards-rack::before {
- content: "\FB3E";
-}
-.mdi-bing::before {
- content: "\F0A4";
-}
-.mdi-binoculars::before {
- content: "\F0A5";
-}
-.mdi-bio::before {
- content: "\F0A6";
-}
-.mdi-biohazard::before {
- content: "\F0A7";
-}
-.mdi-bitbucket::before {
- content: "\F0A8";
-}
-.mdi-bitcoin::before {
- content: "\F812";
-}
-.mdi-black-mesa::before {
- content: "\F0A9";
-}
-.mdi-blackberry::before {
- content: "\F0AA";
-}
-.mdi-blender::before {
- content: "\FCC7";
-}
-.mdi-blender-software::before {
- content: "\F0AB";
-}
-.mdi-blinds::before {
- content: "\F0AC";
-}
-.mdi-blinds-open::before {
- content: "\F0033";
-}
-.mdi-block-helper::before {
- content: "\F0AD";
-}
-.mdi-blogger::before {
- content: "\F0AE";
-}
-.mdi-blood-bag::before {
- content: "\FCC8";
-}
-.mdi-bluetooth::before {
- content: "\F0AF";
-}
-.mdi-bluetooth-audio::before {
- content: "\F0B0";
-}
-.mdi-bluetooth-connect::before {
- content: "\F0B1";
-}
-.mdi-bluetooth-off::before {
- content: "\F0B2";
-}
-.mdi-bluetooth-settings::before {
- content: "\F0B3";
-}
-.mdi-bluetooth-transfer::before {
- content: "\F0B4";
-}
-.mdi-blur::before {
- content: "\F0B5";
-}
-.mdi-blur-linear::before {
- content: "\F0B6";
-}
-.mdi-blur-off::before {
- content: "\F0B7";
-}
-.mdi-blur-radial::before {
- content: "\F0B8";
-}
-.mdi-bolnisi-cross::before {
- content: "\FCC9";
-}
-.mdi-bolt::before {
- content: "\FD8F";
-}
-.mdi-bomb::before {
- content: "\F690";
-}
-.mdi-bomb-off::before {
- content: "\F6C4";
-}
-.mdi-bone::before {
- content: "\F0B9";
-}
-.mdi-book::before {
- content: "\F0BA";
-}
-.mdi-book-information-variant::before {
- content: "\F009A";
-}
-.mdi-book-lock::before {
- content: "\F799";
-}
-.mdi-book-lock-open::before {
- content: "\F79A";
-}
-.mdi-book-minus::before {
- content: "\F5D9";
-}
-.mdi-book-minus-multiple::before {
- content: "\FA93";
-}
-.mdi-book-multiple::before {
- content: "\F0BB";
-}
-.mdi-book-open::before {
- content: "\F0BD";
-}
-.mdi-book-open-outline::before {
- content: "\FB3F";
-}
-.mdi-book-open-page-variant::before {
- content: "\F5DA";
-}
-.mdi-book-open-variant::before {
- content: "\F0BE";
-}
-.mdi-book-outline::before {
- content: "\FB40";
-}
-.mdi-book-play::before {
- content: "\FE9F";
-}
-.mdi-book-play-outline::before {
- content: "\FEA0";
-}
-.mdi-book-plus::before {
- content: "\F5DB";
-}
-.mdi-book-plus-multiple::before {
- content: "\FA94";
-}
-.mdi-book-remove::before {
- content: "\FA96";
-}
-.mdi-book-remove-multiple::before {
- content: "\FA95";
-}
-.mdi-book-search::before {
- content: "\FEA1";
-}
-.mdi-book-search-outline::before {
- content: "\FEA2";
-}
-.mdi-book-variant::before {
- content: "\F0BF";
-}
-.mdi-book-variant-multiple::before {
- content: "\F0BC";
-}
-.mdi-bookmark::before {
- content: "\F0C0";
-}
-.mdi-bookmark-check::before {
- content: "\F0C1";
-}
-.mdi-bookmark-check-outline::before {
- content: "\F03A6";
-}
-.mdi-bookmark-minus::before {
- content: "\F9CB";
-}
-.mdi-bookmark-minus-outline::before {
- content: "\F9CC";
-}
-.mdi-bookmark-multiple::before {
- content: "\FDF8";
-}
-.mdi-bookmark-multiple-outline::before {
- content: "\FDF9";
-}
-.mdi-bookmark-music::before {
- content: "\F0C2";
-}
-.mdi-bookmark-music-outline::before {
- content: "\F03A4";
-}
-.mdi-bookmark-off::before {
- content: "\F9CD";
-}
-.mdi-bookmark-off-outline::before {
- content: "\F9CE";
-}
-.mdi-bookmark-outline::before {
- content: "\F0C3";
-}
-.mdi-bookmark-plus::before {
- content: "\F0C5";
-}
-.mdi-bookmark-plus-outline::before {
- content: "\F0C4";
-}
-.mdi-bookmark-remove::before {
- content: "\F0C6";
-}
-.mdi-bookmark-remove-outline::before {
- content: "\F03A5";
-}
-.mdi-bookshelf::before {
- content: "\F028A";
-}
-.mdi-boom-gate::before {
- content: "\FEA3";
-}
-.mdi-boom-gate-alert::before {
- content: "\FEA4";
-}
-.mdi-boom-gate-alert-outline::before {
- content: "\FEA5";
-}
-.mdi-boom-gate-down::before {
- content: "\FEA6";
-}
-.mdi-boom-gate-down-outline::before {
- content: "\FEA7";
-}
-.mdi-boom-gate-outline::before {
- content: "\FEA8";
-}
-.mdi-boom-gate-up::before {
- content: "\FEA9";
-}
-.mdi-boom-gate-up-outline::before {
- content: "\FEAA";
-}
-.mdi-boombox::before {
- content: "\F5DC";
-}
-.mdi-boomerang::before {
- content: "\F00FA";
-}
-.mdi-bootstrap::before {
- content: "\F6C5";
-}
-.mdi-border-all::before {
- content: "\F0C7";
-}
-.mdi-border-all-variant::before {
- content: "\F8A0";
-}
-.mdi-border-bottom::before {
- content: "\F0C8";
-}
-.mdi-border-bottom-variant::before {
- content: "\F8A1";
-}
-.mdi-border-color::before {
- content: "\F0C9";
-}
-.mdi-border-horizontal::before {
- content: "\F0CA";
-}
-.mdi-border-inside::before {
- content: "\F0CB";
-}
-.mdi-border-left::before {
- content: "\F0CC";
-}
-.mdi-border-left-variant::before {
- content: "\F8A2";
-}
-.mdi-border-none::before {
- content: "\F0CD";
-}
-.mdi-border-none-variant::before {
- content: "\F8A3";
-}
-.mdi-border-outside::before {
- content: "\F0CE";
-}
-.mdi-border-right::before {
- content: "\F0CF";
-}
-.mdi-border-right-variant::before {
- content: "\F8A4";
-}
-.mdi-border-style::before {
- content: "\F0D0";
-}
-.mdi-border-top::before {
- content: "\F0D1";
-}
-.mdi-border-top-variant::before {
- content: "\F8A5";
-}
-.mdi-border-vertical::before {
- content: "\F0D2";
-}
-.mdi-bottle-soda::before {
- content: "\F009B";
-}
-.mdi-bottle-soda-classic::before {
- content: "\F009C";
-}
-.mdi-bottle-soda-classic-outline::before {
- content: "\F038E";
-}
-.mdi-bottle-soda-outline::before {
- content: "\F009D";
-}
-.mdi-bottle-tonic::before {
- content: "\F0159";
-}
-.mdi-bottle-tonic-outline::before {
- content: "\F015A";
-}
-.mdi-bottle-tonic-plus::before {
- content: "\F015B";
-}
-.mdi-bottle-tonic-plus-outline::before {
- content: "\F015C";
-}
-.mdi-bottle-tonic-skull::before {
- content: "\F015D";
-}
-.mdi-bottle-tonic-skull-outline::before {
- content: "\F015E";
-}
-.mdi-bottle-wine::before {
- content: "\F853";
-}
-.mdi-bottle-wine-outline::before {
- content: "\F033B";
-}
-.mdi-bow-tie::before {
- content: "\F677";
-}
-.mdi-bowl::before {
- content: "\F617";
-}
-.mdi-bowling::before {
- content: "\F0D3";
-}
-.mdi-box::before {
- content: "\F0D4";
-}
-.mdi-box-cutter::before {
- content: "\F0D5";
-}
-.mdi-box-shadow::before {
- content: "\F637";
-}
-.mdi-boxing-glove::before {
- content: "\FB41";
-}
-.mdi-braille::before {
- content: "\F9CF";
-}
-.mdi-brain::before {
- content: "\F9D0";
-}
-.mdi-bread-slice::before {
- content: "\FCCA";
-}
-.mdi-bread-slice-outline::before {
- content: "\FCCB";
-}
-.mdi-bridge::before {
- content: "\F618";
-}
-.mdi-briefcase::before {
- content: "\F0D6";
-}
-.mdi-briefcase-account::before {
- content: "\FCCC";
-}
-.mdi-briefcase-account-outline::before {
- content: "\FCCD";
-}
-.mdi-briefcase-check::before {
- content: "\F0D7";
-}
-.mdi-briefcase-check-outline::before {
- content: "\F0349";
-}
-.mdi-briefcase-clock::before {
- content: "\F00FB";
-}
-.mdi-briefcase-clock-outline::before {
- content: "\F00FC";
-}
-.mdi-briefcase-download::before {
- content: "\F0D8";
-}
-.mdi-briefcase-download-outline::before {
- content: "\FC19";
-}
-.mdi-briefcase-edit::before {
- content: "\FA97";
-}
-.mdi-briefcase-edit-outline::before {
- content: "\FC1A";
-}
-.mdi-briefcase-minus::before {
- content: "\FA29";
-}
-.mdi-briefcase-minus-outline::before {
- content: "\FC1B";
-}
-.mdi-briefcase-outline::before {
- content: "\F813";
-}
-.mdi-briefcase-plus::before {
- content: "\FA2A";
-}
-.mdi-briefcase-plus-outline::before {
- content: "\FC1C";
-}
-.mdi-briefcase-remove::before {
- content: "\FA2B";
-}
-.mdi-briefcase-remove-outline::before {
- content: "\FC1D";
-}
-.mdi-briefcase-search::before {
- content: "\FA2C";
-}
-.mdi-briefcase-search-outline::before {
- content: "\FC1E";
-}
-.mdi-briefcase-upload::before {
- content: "\F0D9";
-}
-.mdi-briefcase-upload-outline::before {
- content: "\FC1F";
-}
-.mdi-brightness-1::before {
- content: "\F0DA";
-}
-.mdi-brightness-2::before {
- content: "\F0DB";
-}
-.mdi-brightness-3::before {
- content: "\F0DC";
-}
-.mdi-brightness-4::before {
- content: "\F0DD";
-}
-.mdi-brightness-5::before {
- content: "\F0DE";
-}
-.mdi-brightness-6::before {
- content: "\F0DF";
-}
-.mdi-brightness-7::before {
- content: "\F0E0";
-}
-.mdi-brightness-auto::before {
- content: "\F0E1";
-}
-.mdi-brightness-percent::before {
- content: "\FCCE";
-}
-.mdi-broom::before {
- content: "\F0E2";
-}
-.mdi-brush::before {
- content: "\F0E3";
-}
-.mdi-buddhism::before {
- content: "\F94A";
-}
-.mdi-buffer::before {
- content: "\F619";
-}
-.mdi-bug::before {
- content: "\F0E4";
-}
-.mdi-bug-check::before {
- content: "\FA2D";
-}
-.mdi-bug-check-outline::before {
- content: "\FA2E";
-}
-.mdi-bug-outline::before {
- content: "\FA2F";
-}
-.mdi-bugle::before {
- content: "\FD90";
-}
-.mdi-bulldozer::before {
- content: "\FB07";
-}
-.mdi-bullet::before {
- content: "\FCCF";
-}
-.mdi-bulletin-board::before {
- content: "\F0E5";
-}
-.mdi-bullhorn::before {
- content: "\F0E6";
-}
-.mdi-bullhorn-outline::before {
- content: "\FB08";
-}
-.mdi-bullseye::before {
- content: "\F5DD";
-}
-.mdi-bullseye-arrow::before {
- content: "\F8C8";
-}
-.mdi-bulma::before {
- content: "\F0312";
-}
-.mdi-bunk-bed::before {
- content: "\F032D";
-}
-.mdi-bus::before {
- content: "\F0E7";
-}
-.mdi-bus-alert::before {
- content: "\FA98";
-}
-.mdi-bus-articulated-end::before {
- content: "\F79B";
-}
-.mdi-bus-articulated-front::before {
- content: "\F79C";
-}
-.mdi-bus-clock::before {
- content: "\F8C9";
-}
-.mdi-bus-double-decker::before {
- content: "\F79D";
-}
-.mdi-bus-marker::before {
- content: "\F023D";
-}
-.mdi-bus-multiple::before {
- content: "\FF5C";
-}
-.mdi-bus-school::before {
- content: "\F79E";
-}
-.mdi-bus-side::before {
- content: "\F79F";
-}
-.mdi-bus-stop::before {
- content: "\F0034";
-}
-.mdi-bus-stop-covered::before {
- content: "\F0035";
-}
-.mdi-bus-stop-uncovered::before {
- content: "\F0036";
-}
-.mdi-cached::before {
- content: "\F0E8";
-}
-.mdi-cactus::before {
- content: "\FD91";
-}
-.mdi-cake::before {
- content: "\F0E9";
-}
-.mdi-cake-layered::before {
- content: "\F0EA";
-}
-.mdi-cake-variant::before {
- content: "\F0EB";
-}
-.mdi-calculator::before {
- content: "\F0EC";
-}
-.mdi-calculator-variant::before {
- content: "\FA99";
-}
-.mdi-calendar::before {
- content: "\F0ED";
-}
-.mdi-calendar-account::before {
- content: "\FEF4";
-}
-.mdi-calendar-account-outline::before {
- content: "\FEF5";
-}
-.mdi-calendar-alert::before {
- content: "\FA30";
-}
-.mdi-calendar-arrow-left::before {
- content: "\F015F";
-}
-.mdi-calendar-arrow-right::before {
- content: "\F0160";
-}
-.mdi-calendar-blank::before {
- content: "\F0EE";
-}
-.mdi-calendar-blank-multiple::before {
- content: "\F009E";
-}
-.mdi-calendar-blank-outline::before {
- content: "\FB42";
-}
-.mdi-calendar-check::before {
- content: "\F0EF";
-}
-.mdi-calendar-check-outline::before {
- content: "\FC20";
-}
-.mdi-calendar-clock::before {
- content: "\F0F0";
-}
-.mdi-calendar-edit::before {
- content: "\F8A6";
-}
-.mdi-calendar-export::before {
- content: "\FB09";
-}
-.mdi-calendar-heart::before {
- content: "\F9D1";
-}
-.mdi-calendar-import::before {
- content: "\FB0A";
-}
-.mdi-calendar-minus::before {
- content: "\FD38";
-}
-.mdi-calendar-month::before {
- content: "\FDFA";
-}
-.mdi-calendar-month-outline::before {
- content: "\FDFB";
-}
-.mdi-calendar-multiple::before {
- content: "\F0F1";
-}
-.mdi-calendar-multiple-check::before {
- content: "\F0F2";
-}
-.mdi-calendar-multiselect::before {
- content: "\FA31";
-}
-.mdi-calendar-outline::before {
- content: "\FB43";
-}
-.mdi-calendar-plus::before {
- content: "\F0F3";
-}
-.mdi-calendar-question::before {
- content: "\F691";
-}
-.mdi-calendar-range::before {
- content: "\F678";
-}
-.mdi-calendar-range-outline::before {
- content: "\FB44";
-}
-.mdi-calendar-remove::before {
- content: "\F0F4";
-}
-.mdi-calendar-remove-outline::before {
- content: "\FC21";
-}
-.mdi-calendar-repeat::before {
- content: "\FEAB";
-}
-.mdi-calendar-repeat-outline::before {
- content: "\FEAC";
-}
-.mdi-calendar-search::before {
- content: "\F94B";
-}
-.mdi-calendar-star::before {
- content: "\F9D2";
-}
-.mdi-calendar-text::before {
- content: "\F0F5";
-}
-.mdi-calendar-text-outline::before {
- content: "\FC22";
-}
-.mdi-calendar-today::before {
- content: "\F0F6";
-}
-.mdi-calendar-week::before {
- content: "\FA32";
-}
-.mdi-calendar-week-begin::before {
- content: "\FA33";
-}
-.mdi-calendar-weekend::before {
- content: "\FEF6";
-}
-.mdi-calendar-weekend-outline::before {
- content: "\FEF7";
-}
-.mdi-call-made::before {
- content: "\F0F7";
-}
-.mdi-call-merge::before {
- content: "\F0F8";
-}
-.mdi-call-missed::before {
- content: "\F0F9";
-}
-.mdi-call-received::before {
- content: "\F0FA";
-}
-.mdi-call-split::before {
- content: "\F0FB";
-}
-.mdi-camcorder::before {
- content: "\F0FC";
-}
-.mdi-camcorder-box::before {
- content: "\F0FD";
-}
-.mdi-camcorder-box-off::before {
- content: "\F0FE";
-}
-.mdi-camcorder-off::before {
- content: "\F0FF";
-}
-.mdi-camera::before {
- content: "\F100";
-}
-.mdi-camera-account::before {
- content: "\F8CA";
-}
-.mdi-camera-burst::before {
- content: "\F692";
-}
-.mdi-camera-control::before {
- content: "\FB45";
-}
-.mdi-camera-enhance::before {
- content: "\F101";
-}
-.mdi-camera-enhance-outline::before {
- content: "\FB46";
-}
-.mdi-camera-front::before {
- content: "\F102";
-}
-.mdi-camera-front-variant::before {
- content: "\F103";
-}
-.mdi-camera-gopro::before {
- content: "\F7A0";
-}
-.mdi-camera-image::before {
- content: "\F8CB";
-}
-.mdi-camera-iris::before {
- content: "\F104";
-}
-.mdi-camera-metering-center::before {
- content: "\F7A1";
-}
-.mdi-camera-metering-matrix::before {
- content: "\F7A2";
-}
-.mdi-camera-metering-partial::before {
- content: "\F7A3";
-}
-.mdi-camera-metering-spot::before {
- content: "\F7A4";
-}
-.mdi-camera-off::before {
- content: "\F5DF";
-}
-.mdi-camera-outline::before {
- content: "\FD39";
-}
-.mdi-camera-party-mode::before {
- content: "\F105";
-}
-.mdi-camera-plus::before {
- content: "\FEF8";
-}
-.mdi-camera-plus-outline::before {
- content: "\FEF9";
-}
-.mdi-camera-rear::before {
- content: "\F106";
-}
-.mdi-camera-rear-variant::before {
- content: "\F107";
-}
-.mdi-camera-retake::before {
- content: "\FDFC";
-}
-.mdi-camera-retake-outline::before {
- content: "\FDFD";
-}
-.mdi-camera-switch::before {
- content: "\F108";
-}
-.mdi-camera-timer::before {
- content: "\F109";
-}
-.mdi-camera-wireless::before {
- content: "\FD92";
-}
-.mdi-camera-wireless-outline::before {
- content: "\FD93";
-}
-.mdi-campfire::before {
- content: "\FEFA";
-}
-.mdi-cancel::before {
- content: "\F739";
-}
-.mdi-candle::before {
- content: "\F5E2";
-}
-.mdi-candycane::before {
- content: "\F10A";
-}
-.mdi-cannabis::before {
- content: "\F7A5";
-}
-.mdi-caps-lock::before {
- content: "\FA9A";
-}
-.mdi-car::before {
- content: "\F10B";
-}
-.mdi-car-2-plus::before {
- content: "\F0037";
-}
-.mdi-car-3-plus::before {
- content: "\F0038";
-}
-.mdi-car-back::before {
- content: "\FDFE";
-}
-.mdi-car-battery::before {
- content: "\F10C";
-}
-.mdi-car-brake-abs::before {
- content: "\FC23";
-}
-.mdi-car-brake-alert::before {
- content: "\FC24";
-}
-.mdi-car-brake-hold::before {
- content: "\FD3A";
-}
-.mdi-car-brake-parking::before {
- content: "\FD3B";
-}
-.mdi-car-brake-retarder::before {
- content: "\F0039";
-}
-.mdi-car-child-seat::before {
- content: "\FFC3";
-}
-.mdi-car-clutch::before {
- content: "\F003A";
-}
-.mdi-car-connected::before {
- content: "\F10D";
-}
-.mdi-car-convertible::before {
- content: "\F7A6";
-}
-.mdi-car-coolant-level::before {
- content: "\F003B";
-}
-.mdi-car-cruise-control::before {
- content: "\FD3C";
-}
-.mdi-car-defrost-front::before {
- content: "\FD3D";
-}
-.mdi-car-defrost-rear::before {
- content: "\FD3E";
-}
-.mdi-car-door::before {
- content: "\FB47";
-}
-.mdi-car-door-lock::before {
- content: "\F00C8";
-}
-.mdi-car-electric::before {
- content: "\FB48";
-}
-.mdi-car-esp::before {
- content: "\FC25";
-}
-.mdi-car-estate::before {
- content: "\F7A7";
-}
-.mdi-car-hatchback::before {
- content: "\F7A8";
-}
-.mdi-car-info::before {
- content: "\F01E9";
-}
-.mdi-car-key::before {
- content: "\FB49";
-}
-.mdi-car-light-dimmed::before {
- content: "\FC26";
-}
-.mdi-car-light-fog::before {
- content: "\FC27";
-}
-.mdi-car-light-high::before {
- content: "\FC28";
-}
-.mdi-car-limousine::before {
- content: "\F8CC";
-}
-.mdi-car-multiple::before {
- content: "\FB4A";
-}
-.mdi-car-off::before {
- content: "\FDFF";
-}
-.mdi-car-parking-lights::before {
- content: "\FD3F";
-}
-.mdi-car-pickup::before {
- content: "\F7A9";
-}
-.mdi-car-seat::before {
- content: "\FFC4";
-}
-.mdi-car-seat-cooler::before {
- content: "\FFC5";
-}
-.mdi-car-seat-heater::before {
- content: "\FFC6";
-}
-.mdi-car-shift-pattern::before {
- content: "\FF5D";
-}
-.mdi-car-side::before {
- content: "\F7AA";
-}
-.mdi-car-sports::before {
- content: "\F7AB";
-}
-.mdi-car-tire-alert::before {
- content: "\FC29";
-}
-.mdi-car-traction-control::before {
- content: "\FD40";
-}
-.mdi-car-turbocharger::before {
- content: "\F003C";
-}
-.mdi-car-wash::before {
- content: "\F10E";
-}
-.mdi-car-windshield::before {
- content: "\F003D";
-}
-.mdi-car-windshield-outline::before {
- content: "\F003E";
-}
-.mdi-caravan::before {
- content: "\F7AC";
-}
-.mdi-card::before {
- content: "\FB4B";
-}
-.mdi-card-bulleted::before {
- content: "\FB4C";
-}
-.mdi-card-bulleted-off::before {
- content: "\FB4D";
-}
-.mdi-card-bulleted-off-outline::before {
- content: "\FB4E";
-}
-.mdi-card-bulleted-outline::before {
- content: "\FB4F";
-}
-.mdi-card-bulleted-settings::before {
- content: "\FB50";
-}
-.mdi-card-bulleted-settings-outline::before {
- content: "\FB51";
-}
-.mdi-card-outline::before {
- content: "\FB52";
-}
-.mdi-card-plus::before {
- content: "\F022A";
-}
-.mdi-card-plus-outline::before {
- content: "\F022B";
-}
-.mdi-card-search::before {
- content: "\F009F";
-}
-.mdi-card-search-outline::before {
- content: "\F00A0";
-}
-.mdi-card-text::before {
- content: "\FB53";
-}
-.mdi-card-text-outline::before {
- content: "\FB54";
-}
-.mdi-cards::before {
- content: "\F638";
-}
-.mdi-cards-club::before {
- content: "\F8CD";
-}
-.mdi-cards-diamond::before {
- content: "\F8CE";
-}
-.mdi-cards-diamond-outline::before {
- content: "\F003F";
-}
-.mdi-cards-heart::before {
- content: "\F8CF";
-}
-.mdi-cards-outline::before {
- content: "\F639";
-}
-.mdi-cards-playing-outline::before {
- content: "\F63A";
-}
-.mdi-cards-spade::before {
- content: "\F8D0";
-}
-.mdi-cards-variant::before {
- content: "\F6C6";
-}
-.mdi-carrot::before {
- content: "\F10F";
-}
-.mdi-cart::before {
- content: "\F110";
-}
-.mdi-cart-arrow-down::before {
- content: "\FD42";
-}
-.mdi-cart-arrow-right::before {
- content: "\FC2A";
-}
-.mdi-cart-arrow-up::before {
- content: "\FD43";
-}
-.mdi-cart-minus::before {
- content: "\FD44";
-}
-.mdi-cart-off::before {
- content: "\F66B";
-}
-.mdi-cart-outline::before {
- content: "\F111";
-}
-.mdi-cart-plus::before {
- content: "\F112";
-}
-.mdi-cart-remove::before {
- content: "\FD45";
-}
-.mdi-case-sensitive-alt::before {
- content: "\F113";
-}
-.mdi-cash::before {
- content: "\F114";
-}
-.mdi-cash-100::before {
- content: "\F115";
-}
-.mdi-cash-marker::before {
- content: "\FD94";
-}
-.mdi-cash-minus::before {
- content: "\F028B";
-}
-.mdi-cash-multiple::before {
- content: "\F116";
-}
-.mdi-cash-plus::before {
- content: "\F028C";
-}
-.mdi-cash-refund::before {
- content: "\FA9B";
-}
-.mdi-cash-register::before {
- content: "\FCD0";
-}
-.mdi-cash-remove::before {
- content: "\F028D";
-}
-.mdi-cash-usd::before {
- content: "\F01A1";
-}
-.mdi-cash-usd-outline::before {
- content: "\F117";
-}
-.mdi-cassette::before {
- content: "\F9D3";
-}
-.mdi-cast::before {
- content: "\F118";
-}
-.mdi-cast-audio::before {
- content: "\F0040";
-}
-.mdi-cast-connected::before {
- content: "\F119";
-}
-.mdi-cast-education::before {
- content: "\FE6D";
-}
-.mdi-cast-off::before {
- content: "\F789";
-}
-.mdi-castle::before {
- content: "\F11A";
-}
-.mdi-cat::before {
- content: "\F11B";
-}
-.mdi-cctv::before {
- content: "\F7AD";
-}
-.mdi-ceiling-light::before {
- content: "\F768";
-}
-.mdi-cellphone::before {
- content: "\F11C";
-}
-.mdi-cellphone-android::before {
- content: "\F11D";
-}
-.mdi-cellphone-arrow-down::before {
- content: "\F9D4";
-}
-.mdi-cellphone-basic::before {
- content: "\F11E";
-}
-.mdi-cellphone-dock::before {
- content: "\F11F";
-}
-.mdi-cellphone-erase::before {
- content: "\F94C";
-}
-.mdi-cellphone-information::before {
- content: "\FF5E";
-}
-.mdi-cellphone-iphone::before {
- content: "\F120";
-}
-.mdi-cellphone-key::before {
- content: "\F94D";
-}
-.mdi-cellphone-link::before {
- content: "\F121";
-}
-.mdi-cellphone-link-off::before {
- content: "\F122";
-}
-.mdi-cellphone-lock::before {
- content: "\F94E";
-}
-.mdi-cellphone-message::before {
- content: "\F8D2";
-}
-.mdi-cellphone-message-off::before {
- content: "\F00FD";
-}
-.mdi-cellphone-nfc::before {
- content: "\FEAD";
-}
-.mdi-cellphone-nfc-off::before {
- content: "\F0303";
-}
-.mdi-cellphone-off::before {
- content: "\F94F";
-}
-.mdi-cellphone-play::before {
- content: "\F0041";
-}
-.mdi-cellphone-screenshot::before {
- content: "\FA34";
-}
-.mdi-cellphone-settings::before {
- content: "\F123";
-}
-.mdi-cellphone-settings-variant::before {
- content: "\F950";
-}
-.mdi-cellphone-sound::before {
- content: "\F951";
-}
-.mdi-cellphone-text::before {
- content: "\F8D1";
-}
-.mdi-cellphone-wireless::before {
- content: "\F814";
-}
-.mdi-celtic-cross::before {
- content: "\FCD1";
-}
-.mdi-centos::before {
- content: "\F0145";
-}
-.mdi-certificate::before {
- content: "\F124";
-}
-.mdi-certificate-outline::before {
- content: "\F01B3";
-}
-.mdi-chair-rolling::before {
- content: "\FFBA";
-}
-.mdi-chair-school::before {
- content: "\F125";
-}
-.mdi-charity::before {
- content: "\FC2B";
-}
-.mdi-chart-arc::before {
- content: "\F126";
-}
-.mdi-chart-areaspline::before {
- content: "\F127";
-}
-.mdi-chart-areaspline-variant::before {
- content: "\FEAE";
-}
-.mdi-chart-bar::before {
- content: "\F128";
-}
-.mdi-chart-bar-stacked::before {
- content: "\F769";
-}
-.mdi-chart-bell-curve::before {
- content: "\FC2C";
-}
-.mdi-chart-bell-curve-cumulative::before {
- content: "\FFC7";
-}
-.mdi-chart-bubble::before {
- content: "\F5E3";
-}
-.mdi-chart-donut::before {
- content: "\F7AE";
-}
-.mdi-chart-donut-variant::before {
- content: "\F7AF";
-}
-.mdi-chart-gantt::before {
- content: "\F66C";
-}
-.mdi-chart-histogram::before {
- content: "\F129";
-}
-.mdi-chart-line::before {
- content: "\F12A";
-}
-.mdi-chart-line-stacked::before {
- content: "\F76A";
-}
-.mdi-chart-line-variant::before {
- content: "\F7B0";
-}
-.mdi-chart-multiline::before {
- content: "\F8D3";
-}
-.mdi-chart-multiple::before {
- content: "\F023E";
-}
-.mdi-chart-pie::before {
- content: "\F12B";
-}
-.mdi-chart-ppf::before {
- content: "\F03AB";
-}
-.mdi-chart-scatter-plot::before {
- content: "\FEAF";
-}
-.mdi-chart-scatter-plot-hexbin::before {
- content: "\F66D";
-}
-.mdi-chart-snakey::before {
- content: "\F020A";
-}
-.mdi-chart-snakey-variant::before {
- content: "\F020B";
-}
-.mdi-chart-timeline::before {
- content: "\F66E";
-}
-.mdi-chart-timeline-variant::before {
- content: "\FEB0";
-}
-.mdi-chart-tree::before {
- content: "\FEB1";
-}
-.mdi-chat::before {
- content: "\FB55";
-}
-.mdi-chat-alert::before {
- content: "\FB56";
-}
-.mdi-chat-alert-outline::before {
- content: "\F02F4";
-}
-.mdi-chat-outline::before {
- content: "\FEFB";
-}
-.mdi-chat-processing::before {
- content: "\FB57";
-}
-.mdi-chat-processing-outline::before {
- content: "\F02F5";
-}
-.mdi-chat-sleep::before {
- content: "\F02FC";
-}
-.mdi-chat-sleep-outline::before {
- content: "\F02FD";
-}
-.mdi-check::before {
- content: "\F12C";
-}
-.mdi-check-all::before {
- content: "\F12D";
-}
-.mdi-check-bold::before {
- content: "\FE6E";
-}
-.mdi-check-box-multiple-outline::before {
- content: "\FC2D";
-}
-.mdi-check-box-outline::before {
- content: "\FC2E";
-}
-.mdi-check-circle::before {
- content: "\F5E0";
-}
-.mdi-check-circle-outline::before {
- content: "\F5E1";
-}
-.mdi-check-decagram::before {
- content: "\F790";
-}
-.mdi-check-network::before {
- content: "\FC2F";
-}
-.mdi-check-network-outline::before {
- content: "\FC30";
-}
-.mdi-check-outline::before {
- content: "\F854";
-}
-.mdi-check-underline::before {
- content: "\FE70";
-}
-.mdi-check-underline-circle::before {
- content: "\FE71";
-}
-.mdi-check-underline-circle-outline::before {
- content: "\FE72";
-}
-.mdi-checkbook::before {
- content: "\FA9C";
-}
-.mdi-checkbox-blank::before {
- content: "\F12E";
-}
-.mdi-checkbox-blank-circle::before {
- content: "\F12F";
-}
-.mdi-checkbox-blank-circle-outline::before {
- content: "\F130";
-}
-.mdi-checkbox-blank-off::before {
- content: "\F0317";
-}
-.mdi-checkbox-blank-off-outline::before {
- content: "\F0318";
-}
-.mdi-checkbox-blank-outline::before {
- content: "\F131";
-}
-.mdi-checkbox-intermediate::before {
- content: "\F855";
-}
-.mdi-checkbox-marked::before {
- content: "\F132";
-}
-.mdi-checkbox-marked-circle::before {
- content: "\F133";
-}
-.mdi-checkbox-marked-circle-outline::before {
- content: "\F134";
-}
-.mdi-checkbox-marked-outline::before {
- content: "\F135";
-}
-.mdi-checkbox-multiple-blank::before {
- content: "\F136";
-}
-.mdi-checkbox-multiple-blank-circle::before {
- content: "\F63B";
-}
-.mdi-checkbox-multiple-blank-circle-outline::before {
- content: "\F63C";
-}
-.mdi-checkbox-multiple-blank-outline::before {
- content: "\F137";
-}
-.mdi-checkbox-multiple-marked::before {
- content: "\F138";
-}
-.mdi-checkbox-multiple-marked-circle::before {
- content: "\F63D";
-}
-.mdi-checkbox-multiple-marked-circle-outline::before {
- content: "\F63E";
-}
-.mdi-checkbox-multiple-marked-outline::before {
- content: "\F139";
-}
-.mdi-checkerboard::before {
- content: "\F13A";
-}
-.mdi-checkerboard-minus::before {
- content: "\F022D";
-}
-.mdi-checkerboard-plus::before {
- content: "\F022C";
-}
-.mdi-checkerboard-remove::before {
- content: "\F022E";
-}
-.mdi-cheese::before {
- content: "\F02E4";
-}
-.mdi-chef-hat::before {
- content: "\FB58";
-}
-.mdi-chemical-weapon::before {
- content: "\F13B";
-}
-.mdi-chess-bishop::before {
- content: "\F85B";
-}
-.mdi-chess-king::before {
- content: "\F856";
-}
-.mdi-chess-knight::before {
- content: "\F857";
-}
-.mdi-chess-pawn::before {
- content: "\F858";
-}
-.mdi-chess-queen::before {
- content: "\F859";
-}
-.mdi-chess-rook::before {
- content: "\F85A";
-}
-.mdi-chevron-double-down::before {
- content: "\F13C";
-}
-.mdi-chevron-double-left::before {
- content: "\F13D";
-}
-.mdi-chevron-double-right::before {
- content: "\F13E";
-}
-.mdi-chevron-double-up::before {
- content: "\F13F";
-}
-.mdi-chevron-down::before {
- content: "\F140";
-}
-.mdi-chevron-down-box::before {
- content: "\F9D5";
-}
-.mdi-chevron-down-box-outline::before {
- content: "\F9D6";
-}
-.mdi-chevron-down-circle::before {
- content: "\FB0B";
-}
-.mdi-chevron-down-circle-outline::before {
- content: "\FB0C";
-}
-.mdi-chevron-left::before {
- content: "\F141";
-}
-.mdi-chevron-left-box::before {
- content: "\F9D7";
-}
-.mdi-chevron-left-box-outline::before {
- content: "\F9D8";
-}
-.mdi-chevron-left-circle::before {
- content: "\FB0D";
-}
-.mdi-chevron-left-circle-outline::before {
- content: "\FB0E";
-}
-.mdi-chevron-right::before {
- content: "\F142";
-}
-.mdi-chevron-right-box::before {
- content: "\F9D9";
-}
-.mdi-chevron-right-box-outline::before {
- content: "\F9DA";
-}
-.mdi-chevron-right-circle::before {
- content: "\FB0F";
-}
-.mdi-chevron-right-circle-outline::before {
- content: "\FB10";
-}
-.mdi-chevron-triple-down::before {
- content: "\FD95";
-}
-.mdi-chevron-triple-left::before {
- content: "\FD96";
-}
-.mdi-chevron-triple-right::before {
- content: "\FD97";
-}
-.mdi-chevron-triple-up::before {
- content: "\FD98";
-}
-.mdi-chevron-up::before {
- content: "\F143";
-}
-.mdi-chevron-up-box::before {
- content: "\F9DB";
-}
-.mdi-chevron-up-box-outline::before {
- content: "\F9DC";
-}
-.mdi-chevron-up-circle::before {
- content: "\FB11";
-}
-.mdi-chevron-up-circle-outline::before {
- content: "\FB12";
-}
-.mdi-chili-hot::before {
- content: "\F7B1";
-}
-.mdi-chili-medium::before {
- content: "\F7B2";
-}
-.mdi-chili-mild::before {
- content: "\F7B3";
-}
-.mdi-chip::before {
- content: "\F61A";
-}
-.mdi-christianity::before {
- content: "\F952";
-}
-.mdi-christianity-outline::before {
- content: "\FCD2";
-}
-.mdi-church::before {
- content: "\F144";
-}
-.mdi-cigar::before {
- content: "\F01B4";
-}
-.mdi-circle::before {
- content: "\F764";
-}
-.mdi-circle-double::before {
- content: "\FEB2";
-}
-.mdi-circle-edit-outline::before {
- content: "\F8D4";
-}
-.mdi-circle-expand::before {
- content: "\FEB3";
-}
-.mdi-circle-medium::before {
- content: "\F9DD";
-}
-.mdi-circle-off-outline::before {
- content: "\F00FE";
-}
-.mdi-circle-outline::before {
- content: "\F765";
-}
-.mdi-circle-slice-1::before {
- content: "\FA9D";
-}
-.mdi-circle-slice-2::before {
- content: "\FA9E";
-}
-.mdi-circle-slice-3::before {
- content: "\FA9F";
-}
-.mdi-circle-slice-4::before {
- content: "\FAA0";
-}
-.mdi-circle-slice-5::before {
- content: "\FAA1";
-}
-.mdi-circle-slice-6::before {
- content: "\FAA2";
-}
-.mdi-circle-slice-7::before {
- content: "\FAA3";
-}
-.mdi-circle-slice-8::before {
- content: "\FAA4";
-}
-.mdi-circle-small::before {
- content: "\F9DE";
-}
-.mdi-circular-saw::before {
- content: "\FE73";
-}
-.mdi-cisco-webex::before {
- content: "\F145";
-}
-.mdi-city::before {
- content: "\F146";
-}
-.mdi-city-variant::before {
- content: "\FA35";
-}
-.mdi-city-variant-outline::before {
- content: "\FA36";
-}
-.mdi-clipboard::before {
- content: "\F147";
-}
-.mdi-clipboard-account::before {
- content: "\F148";
-}
-.mdi-clipboard-account-outline::before {
- content: "\FC31";
-}
-.mdi-clipboard-alert::before {
- content: "\F149";
-}
-.mdi-clipboard-alert-outline::before {
- content: "\FCD3";
-}
-.mdi-clipboard-arrow-down::before {
- content: "\F14A";
-}
-.mdi-clipboard-arrow-down-outline::before {
- content: "\FC32";
-}
-.mdi-clipboard-arrow-left::before {
- content: "\F14B";
-}
-.mdi-clipboard-arrow-left-outline::before {
- content: "\FCD4";
-}
-.mdi-clipboard-arrow-right::before {
- content: "\FCD5";
-}
-.mdi-clipboard-arrow-right-outline::before {
- content: "\FCD6";
-}
-.mdi-clipboard-arrow-up::before {
- content: "\FC33";
-}
-.mdi-clipboard-arrow-up-outline::before {
- content: "\FC34";
-}
-.mdi-clipboard-check::before {
- content: "\F14C";
-}
-.mdi-clipboard-check-multiple::before {
- content: "\F028E";
-}
-.mdi-clipboard-check-multiple-outline::before {
- content: "\F028F";
-}
-.mdi-clipboard-check-outline::before {
- content: "\F8A7";
-}
-.mdi-clipboard-file::before {
- content: "\F0290";
-}
-.mdi-clipboard-file-outline::before {
- content: "\F0291";
-}
-.mdi-clipboard-flow::before {
- content: "\F6C7";
-}
-.mdi-clipboard-flow-outline::before {
- content: "\F0142";
-}
-.mdi-clipboard-list::before {
- content: "\F00FF";
-}
-.mdi-clipboard-list-outline::before {
- content: "\F0100";
-}
-.mdi-clipboard-multiple::before {
- content: "\F0292";
-}
-.mdi-clipboard-multiple-outline::before {
- content: "\F0293";
-}
-.mdi-clipboard-outline::before {
- content: "\F14D";
-}
-.mdi-clipboard-play::before {
- content: "\FC35";
-}
-.mdi-clipboard-play-multiple::before {
- content: "\F0294";
-}
-.mdi-clipboard-play-multiple-outline::before {
- content: "\F0295";
-}
-.mdi-clipboard-play-outline::before {
- content: "\FC36";
-}
-.mdi-clipboard-plus::before {
- content: "\F750";
-}
-.mdi-clipboard-plus-outline::before {
- content: "\F034A";
-}
-.mdi-clipboard-pulse::before {
- content: "\F85C";
-}
-.mdi-clipboard-pulse-outline::before {
- content: "\F85D";
-}
-.mdi-clipboard-text::before {
- content: "\F14E";
-}
-.mdi-clipboard-text-multiple::before {
- content: "\F0296";
-}
-.mdi-clipboard-text-multiple-outline::before {
- content: "\F0297";
-}
-.mdi-clipboard-text-outline::before {
- content: "\FA37";
-}
-.mdi-clipboard-text-play::before {
- content: "\FC37";
-}
-.mdi-clipboard-text-play-outline::before {
- content: "\FC38";
-}
-.mdi-clippy::before {
- content: "\F14F";
-}
-.mdi-clock::before {
- content: "\F953";
-}
-.mdi-clock-alert::before {
- content: "\F954";
-}
-.mdi-clock-alert-outline::before {
- content: "\F5CE";
-}
-.mdi-clock-check::before {
- content: "\FFC8";
-}
-.mdi-clock-check-outline::before {
- content: "\FFC9";
-}
-.mdi-clock-digital::before {
- content: "\FEB4";
-}
-.mdi-clock-end::before {
- content: "\F151";
-}
-.mdi-clock-fast::before {
- content: "\F152";
-}
-.mdi-clock-in::before {
- content: "\F153";
-}
-.mdi-clock-out::before {
- content: "\F154";
-}
-.mdi-clock-outline::before {
- content: "\F150";
-}
-.mdi-clock-start::before {
- content: "\F155";
-}
-.mdi-close::before {
- content: "\F156";
-}
-.mdi-close-box::before {
- content: "\F157";
-}
-.mdi-close-box-multiple::before {
- content: "\FC39";
-}
-.mdi-close-box-multiple-outline::before {
- content: "\FC3A";
-}
-.mdi-close-box-outline::before {
- content: "\F158";
-}
-.mdi-close-circle::before {
- content: "\F159";
-}
-.mdi-close-circle-outline::before {
- content: "\F15A";
-}
-.mdi-close-network::before {
- content: "\F15B";
-}
-.mdi-close-network-outline::before {
- content: "\FC3B";
-}
-.mdi-close-octagon::before {
- content: "\F15C";
-}
-.mdi-close-octagon-outline::before {
- content: "\F15D";
-}
-.mdi-close-outline::before {
- content: "\F6C8";
-}
-.mdi-closed-caption::before {
- content: "\F15E";
-}
-.mdi-closed-caption-outline::before {
- content: "\FD99";
-}
-.mdi-cloud::before {
- content: "\F15F";
-}
-.mdi-cloud-alert::before {
- content: "\F9DF";
-}
-.mdi-cloud-braces::before {
- content: "\F7B4";
-}
-.mdi-cloud-check::before {
- content: "\F160";
-}
-.mdi-cloud-check-outline::before {
- content: "\F02F7";
-}
-.mdi-cloud-circle::before {
- content: "\F161";
-}
-.mdi-cloud-download::before {
- content: "\F162";
-}
-.mdi-cloud-download-outline::before {
- content: "\FB59";
-}
-.mdi-cloud-lock::before {
- content: "\F021C";
-}
-.mdi-cloud-lock-outline::before {
- content: "\F021D";
-}
-.mdi-cloud-off-outline::before {
- content: "\F164";
-}
-.mdi-cloud-outline::before {
- content: "\F163";
-}
-.mdi-cloud-print::before {
- content: "\F165";
-}
-.mdi-cloud-print-outline::before {
- content: "\F166";
-}
-.mdi-cloud-question::before {
- content: "\FA38";
-}
-.mdi-cloud-search::before {
- content: "\F955";
-}
-.mdi-cloud-search-outline::before {
- content: "\F956";
-}
-.mdi-cloud-sync::before {
- content: "\F63F";
-}
-.mdi-cloud-sync-outline::before {
- content: "\F0301";
-}
-.mdi-cloud-tags::before {
- content: "\F7B5";
-}
-.mdi-cloud-upload::before {
- content: "\F167";
-}
-.mdi-cloud-upload-outline::before {
- content: "\FB5A";
-}
-.mdi-clover::before {
- content: "\F815";
-}
-.mdi-coach-lamp::before {
- content: "\F0042";
-}
-.mdi-coat-rack::before {
- content: "\F00C9";
-}
-.mdi-code-array::before {
- content: "\F168";
-}
-.mdi-code-braces::before {
- content: "\F169";
-}
-.mdi-code-braces-box::before {
- content: "\F0101";
-}
-.mdi-code-brackets::before {
- content: "\F16A";
-}
-.mdi-code-equal::before {
- content: "\F16B";
-}
-.mdi-code-greater-than::before {
- content: "\F16C";
-}
-.mdi-code-greater-than-or-equal::before {
- content: "\F16D";
-}
-.mdi-code-less-than::before {
- content: "\F16E";
-}
-.mdi-code-less-than-or-equal::before {
- content: "\F16F";
-}
-.mdi-code-not-equal::before {
- content: "\F170";
-}
-.mdi-code-not-equal-variant::before {
- content: "\F171";
-}
-.mdi-code-parentheses::before {
- content: "\F172";
-}
-.mdi-code-parentheses-box::before {
- content: "\F0102";
-}
-.mdi-code-string::before {
- content: "\F173";
-}
-.mdi-code-tags::before {
- content: "\F174";
-}
-.mdi-code-tags-check::before {
- content: "\F693";
-}
-.mdi-codepen::before {
- content: "\F175";
-}
-.mdi-coffee::before {
- content: "\F176";
-}
-.mdi-coffee-maker::before {
- content: "\F00CA";
-}
-.mdi-coffee-off::before {
- content: "\FFCA";
-}
-.mdi-coffee-off-outline::before {
- content: "\FFCB";
-}
-.mdi-coffee-outline::before {
- content: "\F6C9";
-}
-.mdi-coffee-to-go::before {
- content: "\F177";
-}
-.mdi-coffee-to-go-outline::before {
- content: "\F0339";
-}
-.mdi-coffin::before {
- content: "\FB5B";
-}
-.mdi-cog-clockwise::before {
- content: "\F0208";
-}
-.mdi-cog-counterclockwise::before {
- content: "\F0209";
-}
-.mdi-cogs::before {
- content: "\F8D5";
-}
-.mdi-coin::before {
- content: "\F0196";
-}
-.mdi-coin-outline::before {
- content: "\F178";
-}
-.mdi-coins::before {
- content: "\F694";
-}
-.mdi-collage::before {
- content: "\F640";
-}
-.mdi-collapse-all::before {
- content: "\FAA5";
-}
-.mdi-collapse-all-outline::before {
- content: "\FAA6";
-}
-.mdi-color-helper::before {
- content: "\F179";
-}
-.mdi-comma::before {
- content: "\FE74";
-}
-.mdi-comma-box::before {
- content: "\FE75";
-}
-.mdi-comma-box-outline::before {
- content: "\FE76";
-}
-.mdi-comma-circle::before {
- content: "\FE77";
-}
-.mdi-comma-circle-outline::before {
- content: "\FE78";
-}
-.mdi-comment::before {
- content: "\F17A";
-}
-.mdi-comment-account::before {
- content: "\F17B";
-}
-.mdi-comment-account-outline::before {
- content: "\F17C";
-}
-.mdi-comment-alert::before {
- content: "\F17D";
-}
-.mdi-comment-alert-outline::before {
- content: "\F17E";
-}
-.mdi-comment-arrow-left::before {
- content: "\F9E0";
-}
-.mdi-comment-arrow-left-outline::before {
- content: "\F9E1";
-}
-.mdi-comment-arrow-right::before {
- content: "\F9E2";
-}
-.mdi-comment-arrow-right-outline::before {
- content: "\F9E3";
-}
-.mdi-comment-check::before {
- content: "\F17F";
-}
-.mdi-comment-check-outline::before {
- content: "\F180";
-}
-.mdi-comment-edit::before {
- content: "\F01EA";
-}
-.mdi-comment-edit-outline::before {
- content: "\F02EF";
-}
-.mdi-comment-eye::before {
- content: "\FA39";
-}
-.mdi-comment-eye-outline::before {
- content: "\FA3A";
-}
-.mdi-comment-multiple::before {
- content: "\F85E";
-}
-.mdi-comment-multiple-outline::before {
- content: "\F181";
-}
-.mdi-comment-outline::before {
- content: "\F182";
-}
-.mdi-comment-plus::before {
- content: "\F9E4";
-}
-.mdi-comment-plus-outline::before {
- content: "\F183";
-}
-.mdi-comment-processing::before {
- content: "\F184";
-}
-.mdi-comment-processing-outline::before {
- content: "\F185";
-}
-.mdi-comment-question::before {
- content: "\F816";
-}
-.mdi-comment-question-outline::before {
- content: "\F186";
-}
-.mdi-comment-quote::before {
- content: "\F0043";
-}
-.mdi-comment-quote-outline::before {
- content: "\F0044";
-}
-.mdi-comment-remove::before {
- content: "\F5DE";
-}
-.mdi-comment-remove-outline::before {
- content: "\F187";
-}
-.mdi-comment-search::before {
- content: "\FA3B";
-}
-.mdi-comment-search-outline::before {
- content: "\FA3C";
-}
-.mdi-comment-text::before {
- content: "\F188";
-}
-.mdi-comment-text-multiple::before {
- content: "\F85F";
-}
-.mdi-comment-text-multiple-outline::before {
- content: "\F860";
-}
-.mdi-comment-text-outline::before {
- content: "\F189";
-}
-.mdi-compare::before {
- content: "\F18A";
-}
-.mdi-compass::before {
- content: "\F18B";
-}
-.mdi-compass-off::before {
- content: "\FB5C";
-}
-.mdi-compass-off-outline::before {
- content: "\FB5D";
-}
-.mdi-compass-outline::before {
- content: "\F18C";
-}
-.mdi-compass-rose::before {
- content: "\F03AD";
-}
-.mdi-concourse-ci::before {
- content: "\F00CB";
-}
-.mdi-console::before {
- content: "\F18D";
-}
-.mdi-console-line::before {
- content: "\F7B6";
-}
-.mdi-console-network::before {
- content: "\F8A8";
-}
-.mdi-console-network-outline::before {
- content: "\FC3C";
-}
-.mdi-consolidate::before {
- content: "\F0103";
-}
-.mdi-contact-mail::before {
- content: "\F18E";
-}
-.mdi-contact-mail-outline::before {
- content: "\FEB5";
-}
-.mdi-contact-phone::before {
- content: "\FEB6";
-}
-.mdi-contact-phone-outline::before {
- content: "\FEB7";
-}
-.mdi-contactless-payment::before {
- content: "\FD46";
-}
-.mdi-contacts::before {
- content: "\F6CA";
-}
-.mdi-contain::before {
- content: "\FA3D";
-}
-.mdi-contain-end::before {
- content: "\FA3E";
-}
-.mdi-contain-start::before {
- content: "\FA3F";
-}
-.mdi-content-copy::before {
- content: "\F18F";
-}
-.mdi-content-cut::before {
- content: "\F190";
-}
-.mdi-content-duplicate::before {
- content: "\F191";
-}
-.mdi-content-paste::before {
- content: "\F192";
-}
-.mdi-content-save::before {
- content: "\F193";
-}
-.mdi-content-save-alert::before {
- content: "\FF5F";
-}
-.mdi-content-save-alert-outline::before {
- content: "\FF60";
-}
-.mdi-content-save-all::before {
- content: "\F194";
-}
-.mdi-content-save-all-outline::before {
- content: "\FF61";
-}
-.mdi-content-save-edit::before {
- content: "\FCD7";
-}
-.mdi-content-save-edit-outline::before {
- content: "\FCD8";
-}
-.mdi-content-save-move::before {
- content: "\FE79";
-}
-.mdi-content-save-move-outline::before {
- content: "\FE7A";
-}
-.mdi-content-save-outline::before {
- content: "\F817";
-}
-.mdi-content-save-settings::before {
- content: "\F61B";
-}
-.mdi-content-save-settings-outline::before {
- content: "\FB13";
-}
-.mdi-contrast::before {
- content: "\F195";
-}
-.mdi-contrast-box::before {
- content: "\F196";
-}
-.mdi-contrast-circle::before {
- content: "\F197";
-}
-.mdi-controller-classic::before {
- content: "\FB5E";
-}
-.mdi-controller-classic-outline::before {
- content: "\FB5F";
-}
-.mdi-cookie::before {
- content: "\F198";
-}
-.mdi-coolant-temperature::before {
- content: "\F3C8";
-}
-.mdi-copyright::before {
- content: "\F5E6";
-}
-.mdi-cordova::before {
- content: "\F957";
-}
-.mdi-corn::before {
- content: "\F7B7";
-}
-.mdi-counter::before {
- content: "\F199";
-}
-.mdi-cow::before {
- content: "\F19A";
-}
-.mdi-cowboy::before {
- content: "\FEB8";
-}
-.mdi-cpu-32-bit::before {
- content: "\FEFC";
-}
-.mdi-cpu-64-bit::before {
- content: "\FEFD";
-}
-.mdi-crane::before {
- content: "\F861";
-}
-.mdi-creation::before {
- content: "\F1C9";
-}
-.mdi-creative-commons::before {
- content: "\FD47";
-}
-.mdi-credit-card::before {
- content: "\F0010";
-}
-.mdi-credit-card-clock::before {
- content: "\FEFE";
-}
-.mdi-credit-card-clock-outline::before {
- content: "\FFBC";
-}
-.mdi-credit-card-marker::before {
- content: "\F6A7";
-}
-.mdi-credit-card-marker-outline::before {
- content: "\FD9A";
-}
-.mdi-credit-card-minus::before {
- content: "\FFCC";
-}
-.mdi-credit-card-minus-outline::before {
- content: "\FFCD";
-}
-.mdi-credit-card-multiple::before {
- content: "\F0011";
-}
-.mdi-credit-card-multiple-outline::before {
- content: "\F19C";
-}
-.mdi-credit-card-off::before {
- content: "\F0012";
-}
-.mdi-credit-card-off-outline::before {
- content: "\F5E4";
-}
-.mdi-credit-card-outline::before {
- content: "\F19B";
-}
-.mdi-credit-card-plus::before {
- content: "\F0013";
-}
-.mdi-credit-card-plus-outline::before {
- content: "\F675";
-}
-.mdi-credit-card-refund::before {
- content: "\F0014";
-}
-.mdi-credit-card-refund-outline::before {
- content: "\FAA7";
-}
-.mdi-credit-card-remove::before {
- content: "\FFCE";
-}
-.mdi-credit-card-remove-outline::before {
- content: "\FFCF";
-}
-.mdi-credit-card-scan::before {
- content: "\F0015";
-}
-.mdi-credit-card-scan-outline::before {
- content: "\F19D";
-}
-.mdi-credit-card-settings::before {
- content: "\F0016";
-}
-.mdi-credit-card-settings-outline::before {
- content: "\F8D6";
-}
-.mdi-credit-card-wireless::before {
- content: "\F801";
-}
-.mdi-credit-card-wireless-outline::before {
- content: "\FD48";
-}
-.mdi-cricket::before {
- content: "\FD49";
-}
-.mdi-crop::before {
- content: "\F19E";
-}
-.mdi-crop-free::before {
- content: "\F19F";
-}
-.mdi-crop-landscape::before {
- content: "\F1A0";
-}
-.mdi-crop-portrait::before {
- content: "\F1A1";
-}
-.mdi-crop-rotate::before {
- content: "\F695";
-}
-.mdi-crop-square::before {
- content: "\F1A2";
-}
-.mdi-crosshairs::before {
- content: "\F1A3";
-}
-.mdi-crosshairs-gps::before {
- content: "\F1A4";
-}
-.mdi-crosshairs-off::before {
- content: "\FF62";
-}
-.mdi-crosshairs-question::before {
- content: "\F0161";
-}
-.mdi-crown::before {
- content: "\F1A5";
-}
-.mdi-crown-outline::before {
- content: "\F01FB";
-}
-.mdi-cryengine::before {
- content: "\F958";
-}
-.mdi-crystal-ball::before {
- content: "\FB14";
-}
-.mdi-cube::before {
- content: "\F1A6";
-}
-.mdi-cube-outline::before {
- content: "\F1A7";
-}
-.mdi-cube-scan::before {
- content: "\FB60";
-}
-.mdi-cube-send::before {
- content: "\F1A8";
-}
-.mdi-cube-unfolded::before {
- content: "\F1A9";
-}
-.mdi-cup::before {
- content: "\F1AA";
-}
-.mdi-cup-off::before {
- content: "\F5E5";
-}
-.mdi-cup-off-outline::before {
- content: "\F03A8";
-}
-.mdi-cup-outline::before {
- content: "\F033A";
-}
-.mdi-cup-water::before {
- content: "\F1AB";
-}
-.mdi-cupboard::before {
- content: "\FF63";
-}
-.mdi-cupboard-outline::before {
- content: "\FF64";
-}
-.mdi-cupcake::before {
- content: "\F959";
-}
-.mdi-curling::before {
- content: "\F862";
-}
-.mdi-currency-bdt::before {
- content: "\F863";
-}
-.mdi-currency-brl::before {
- content: "\FB61";
-}
-.mdi-currency-btc::before {
- content: "\F1AC";
-}
-.mdi-currency-cny::before {
- content: "\F7B9";
-}
-.mdi-currency-eth::before {
- content: "\F7BA";
-}
-.mdi-currency-eur::before {
- content: "\F1AD";
-}
-.mdi-currency-eur-off::before {
- content: "\F0340";
-}
-.mdi-currency-gbp::before {
- content: "\F1AE";
-}
-.mdi-currency-ils::before {
- content: "\FC3D";
-}
-.mdi-currency-inr::before {
- content: "\F1AF";
-}
-.mdi-currency-jpy::before {
- content: "\F7BB";
-}
-.mdi-currency-krw::before {
- content: "\F7BC";
-}
-.mdi-currency-kzt::before {
- content: "\F864";
-}
-.mdi-currency-ngn::before {
- content: "\F1B0";
-}
-.mdi-currency-php::before {
- content: "\F9E5";
-}
-.mdi-currency-rial::before {
- content: "\FEB9";
-}
-.mdi-currency-rub::before {
- content: "\F1B1";
-}
-.mdi-currency-sign::before {
- content: "\F7BD";
-}
-.mdi-currency-try::before {
- content: "\F1B2";
-}
-.mdi-currency-twd::before {
- content: "\F7BE";
-}
-.mdi-currency-usd::before {
- content: "\F1B3";
-}
-.mdi-currency-usd-off::before {
- content: "\F679";
-}
-.mdi-current-ac::before {
- content: "\F95A";
-}
-.mdi-current-dc::before {
- content: "\F95B";
-}
-.mdi-cursor-default::before {
- content: "\F1B4";
-}
-.mdi-cursor-default-click::before {
- content: "\FCD9";
-}
-.mdi-cursor-default-click-outline::before {
- content: "\FCDA";
-}
-.mdi-cursor-default-gesture::before {
- content: "\F0152";
-}
-.mdi-cursor-default-gesture-outline::before {
- content: "\F0153";
-}
-.mdi-cursor-default-outline::before {
- content: "\F1B5";
-}
-.mdi-cursor-move::before {
- content: "\F1B6";
-}
-.mdi-cursor-pointer::before {
- content: "\F1B7";
-}
-.mdi-cursor-text::before {
- content: "\F5E7";
-}
-.mdi-database::before {
- content: "\F1B8";
-}
-.mdi-database-check::before {
- content: "\FAA8";
-}
-.mdi-database-edit::before {
- content: "\FB62";
-}
-.mdi-database-export::before {
- content: "\F95D";
-}
-.mdi-database-import::before {
- content: "\F95C";
-}
-.mdi-database-lock::before {
- content: "\FAA9";
-}
-.mdi-database-marker::before {
- content: "\F0321";
-}
-.mdi-database-minus::before {
- content: "\F1B9";
-}
-.mdi-database-plus::before {
- content: "\F1BA";
-}
-.mdi-database-refresh::before {
- content: "\FCDB";
-}
-.mdi-database-remove::before {
- content: "\FCDC";
-}
-.mdi-database-search::before {
- content: "\F865";
-}
-.mdi-database-settings::before {
- content: "\FCDD";
-}
-.mdi-death-star::before {
- content: "\F8D7";
-}
-.mdi-death-star-variant::before {
- content: "\F8D8";
-}
-.mdi-deathly-hallows::before {
- content: "\FB63";
-}
-.mdi-debian::before {
- content: "\F8D9";
-}
-.mdi-debug-step-into::before {
- content: "\F1BB";
-}
-.mdi-debug-step-out::before {
- content: "\F1BC";
-}
-.mdi-debug-step-over::before {
- content: "\F1BD";
-}
-.mdi-decagram::before {
- content: "\F76B";
-}
-.mdi-decagram-outline::before {
- content: "\F76C";
-}
-.mdi-decimal::before {
- content: "\F00CC";
-}
-.mdi-decimal-comma::before {
- content: "\F00CD";
-}
-.mdi-decimal-comma-decrease::before {
- content: "\F00CE";
-}
-.mdi-decimal-comma-increase::before {
- content: "\F00CF";
-}
-.mdi-decimal-decrease::before {
- content: "\F1BE";
-}
-.mdi-decimal-increase::before {
- content: "\F1BF";
-}
-.mdi-delete::before {
- content: "\F1C0";
-}
-.mdi-delete-alert::before {
- content: "\F00D0";
-}
-.mdi-delete-alert-outline::before {
- content: "\F00D1";
-}
-.mdi-delete-circle::before {
- content: "\F682";
-}
-.mdi-delete-circle-outline::before {
- content: "\FB64";
-}
-.mdi-delete-empty::before {
- content: "\F6CB";
-}
-.mdi-delete-empty-outline::before {
- content: "\FEBA";
-}
-.mdi-delete-forever::before {
- content: "\F5E8";
-}
-.mdi-delete-forever-outline::before {
- content: "\FB65";
-}
-.mdi-delete-off::before {
- content: "\F00D2";
-}
-.mdi-delete-off-outline::before {
- content: "\F00D3";
-}
-.mdi-delete-outline::before {
- content: "\F9E6";
-}
-.mdi-delete-restore::before {
- content: "\F818";
-}
-.mdi-delete-sweep::before {
- content: "\F5E9";
-}
-.mdi-delete-sweep-outline::before {
- content: "\FC3E";
-}
-.mdi-delete-variant::before {
- content: "\F1C1";
-}
-.mdi-delta::before {
- content: "\F1C2";
-}
-.mdi-desk::before {
- content: "\F0264";
-}
-.mdi-desk-lamp::before {
- content: "\F95E";
-}
-.mdi-deskphone::before {
- content: "\F1C3";
-}
-.mdi-desktop-classic::before {
- content: "\F7BF";
-}
-.mdi-desktop-mac::before {
- content: "\F1C4";
-}
-.mdi-desktop-mac-dashboard::before {
- content: "\F9E7";
-}
-.mdi-desktop-tower::before {
- content: "\F1C5";
-}
-.mdi-desktop-tower-monitor::before {
- content: "\FAAA";
-}
-.mdi-details::before {
- content: "\F1C6";
-}
-.mdi-dev-to::before {
- content: "\FD4A";
-}
-.mdi-developer-board::before {
- content: "\F696";
-}
-.mdi-deviantart::before {
- content: "\F1C7";
-}
-.mdi-devices::before {
- content: "\FFD0";
-}
-.mdi-diabetes::before {
- content: "\F0151";
-}
-.mdi-dialpad::before {
- content: "\F61C";
-}
-.mdi-diameter::before {
- content: "\FC3F";
-}
-.mdi-diameter-outline::before {
- content: "\FC40";
-}
-.mdi-diameter-variant::before {
- content: "\FC41";
-}
-.mdi-diamond::before {
- content: "\FB66";
-}
-.mdi-diamond-outline::before {
- content: "\FB67";
-}
-.mdi-diamond-stone::before {
- content: "\F1C8";
-}
-.mdi-dice-1::before {
- content: "\F1CA";
-}
-.mdi-dice-1-outline::before {
- content: "\F0175";
-}
-.mdi-dice-2::before {
- content: "\F1CB";
-}
-.mdi-dice-2-outline::before {
- content: "\F0176";
-}
-.mdi-dice-3::before {
- content: "\F1CC";
-}
-.mdi-dice-3-outline::before {
- content: "\F0177";
-}
-.mdi-dice-4::before {
- content: "\F1CD";
-}
-.mdi-dice-4-outline::before {
- content: "\F0178";
-}
-.mdi-dice-5::before {
- content: "\F1CE";
-}
-.mdi-dice-5-outline::before {
- content: "\F0179";
-}
-.mdi-dice-6::before {
- content: "\F1CF";
-}
-.mdi-dice-6-outline::before {
- content: "\F017A";
-}
-.mdi-dice-d10::before {
- content: "\F017E";
-}
-.mdi-dice-d10-outline::before {
- content: "\F76E";
-}
-.mdi-dice-d12::before {
- content: "\F017F";
-}
-.mdi-dice-d12-outline::before {
- content: "\F866";
-}
-.mdi-dice-d20::before {
- content: "\F0180";
-}
-.mdi-dice-d20-outline::before {
- content: "\F5EA";
-}
-.mdi-dice-d4::before {
- content: "\F017B";
-}
-.mdi-dice-d4-outline::before {
- content: "\F5EB";
-}
-.mdi-dice-d6::before {
- content: "\F017C";
-}
-.mdi-dice-d6-outline::before {
- content: "\F5EC";
-}
-.mdi-dice-d8::before {
- content: "\F017D";
-}
-.mdi-dice-d8-outline::before {
- content: "\F5ED";
-}
-.mdi-dice-multiple::before {
- content: "\F76D";
-}
-.mdi-dice-multiple-outline::before {
- content: "\F0181";
-}
-.mdi-dictionary::before {
- content: "\F61D";
-}
-.mdi-digital-ocean::before {
- content: "\F0262";
-}
-.mdi-dip-switch::before {
- content: "\F7C0";
-}
-.mdi-directions::before {
- content: "\F1D0";
-}
-.mdi-directions-fork::before {
- content: "\F641";
-}
-.mdi-disc::before {
- content: "\F5EE";
-}
-.mdi-disc-alert::before {
- content: "\F1D1";
-}
-.mdi-disc-player::before {
- content: "\F95F";
-}
-.mdi-discord::before {
- content: "\F66F";
-}
-.mdi-dishwasher::before {
- content: "\FAAB";
-}
-.mdi-dishwasher-alert::before {
- content: "\F01E3";
-}
-.mdi-dishwasher-off::before {
- content: "\F01E4";
-}
-.mdi-disqus::before {
- content: "\F1D2";
-}
-.mdi-disqus-outline::before {
- content: "\F1D3";
-}
-.mdi-distribute-horizontal-center::before {
- content: "\F01F4";
-}
-.mdi-distribute-horizontal-left::before {
- content: "\F01F3";
-}
-.mdi-distribute-horizontal-right::before {
- content: "\F01F5";
-}
-.mdi-distribute-vertical-bottom::before {
- content: "\F01F6";
-}
-.mdi-distribute-vertical-center::before {
- content: "\F01F7";
-}
-.mdi-distribute-vertical-top::before {
- content: "\F01F8";
-}
-.mdi-diving-flippers::before {
- content: "\FD9B";
-}
-.mdi-diving-helmet::before {
- content: "\FD9C";
-}
-.mdi-diving-scuba::before {
- content: "\FD9D";
-}
-.mdi-diving-scuba-flag::before {
- content: "\FD9E";
-}
-.mdi-diving-scuba-tank::before {
- content: "\FD9F";
-}
-.mdi-diving-scuba-tank-multiple::before {
- content: "\FDA0";
-}
-.mdi-diving-snorkel::before {
- content: "\FDA1";
-}
-.mdi-division::before {
- content: "\F1D4";
-}
-.mdi-division-box::before {
- content: "\F1D5";
-}
-.mdi-dlna::before {
- content: "\FA40";
-}
-.mdi-dna::before {
- content: "\F683";
-}
-.mdi-dns::before {
- content: "\F1D6";
-}
-.mdi-dns-outline::before {
- content: "\FB68";
-}
-.mdi-do-not-disturb::before {
- content: "\F697";
-}
-.mdi-do-not-disturb-off::before {
- content: "\F698";
-}
-.mdi-dock-bottom::before {
- content: "\F00D4";
-}
-.mdi-dock-left::before {
- content: "\F00D5";
-}
-.mdi-dock-right::before {
- content: "\F00D6";
-}
-.mdi-dock-window::before {
- content: "\F00D7";
-}
-.mdi-docker::before {
- content: "\F867";
-}
-.mdi-doctor::before {
- content: "\FA41";
-}
-.mdi-dog::before {
- content: "\FA42";
-}
-.mdi-dog-service::before {
- content: "\FAAC";
-}
-.mdi-dog-side::before {
- content: "\FA43";
-}
-.mdi-dolby::before {
- content: "\F6B2";
-}
-.mdi-dolly::before {
- content: "\FEBB";
-}
-.mdi-domain::before {
- content: "\F1D7";
-}
-.mdi-domain-off::before {
- content: "\FD4B";
-}
-.mdi-domain-plus::before {
- content: "\F00D8";
-}
-.mdi-domain-remove::before {
- content: "\F00D9";
-}
-.mdi-domino-mask::before {
- content: "\F0045";
-}
-.mdi-donkey::before {
- content: "\F7C1";
-}
-.mdi-door::before {
- content: "\F819";
-}
-.mdi-door-closed::before {
- content: "\F81A";
-}
-.mdi-door-closed-lock::before {
- content: "\F00DA";
-}
-.mdi-door-open::before {
- content: "\F81B";
-}
-.mdi-doorbell::before {
- content: "\F0311";
-}
-.mdi-doorbell-video::before {
- content: "\F868";
-}
-.mdi-dot-net::before {
- content: "\FAAD";
-}
-.mdi-dots-horizontal::before {
- content: "\F1D8";
-}
-.mdi-dots-horizontal-circle::before {
- content: "\F7C2";
-}
-.mdi-dots-horizontal-circle-outline::before {
- content: "\FB69";
-}
-.mdi-dots-vertical::before {
- content: "\F1D9";
-}
-.mdi-dots-vertical-circle::before {
- content: "\F7C3";
-}
-.mdi-dots-vertical-circle-outline::before {
- content: "\FB6A";
-}
-.mdi-douban::before {
- content: "\F699";
-}
-.mdi-download::before {
- content: "\F1DA";
-}
-.mdi-download-lock::before {
- content: "\F034B";
-}
-.mdi-download-lock-outline::before {
- content: "\F034C";
-}
-.mdi-download-multiple::before {
- content: "\F9E8";
-}
-.mdi-download-network::before {
- content: "\F6F3";
-}
-.mdi-download-network-outline::before {
- content: "\FC42";
-}
-.mdi-download-off::before {
- content: "\F00DB";
-}
-.mdi-download-off-outline::before {
- content: "\F00DC";
-}
-.mdi-download-outline::before {
- content: "\FB6B";
-}
-.mdi-drag::before {
- content: "\F1DB";
-}
-.mdi-drag-horizontal::before {
- content: "\F1DC";
-}
-.mdi-drag-horizontal-variant::before {
- content: "\F031B";
-}
-.mdi-drag-variant::before {
- content: "\FB6C";
-}
-.mdi-drag-vertical::before {
- content: "\F1DD";
-}
-.mdi-drag-vertical-variant::before {
- content: "\F031C";
-}
-.mdi-drama-masks::before {
- content: "\FCDE";
-}
-.mdi-draw::before {
- content: "\FF66";
-}
-.mdi-drawing::before {
- content: "\F1DE";
-}
-.mdi-drawing-box::before {
- content: "\F1DF";
-}
-.mdi-dresser::before {
- content: "\FF67";
-}
-.mdi-dresser-outline::before {
- content: "\FF68";
-}
-.mdi-dribbble::before {
- content: "\F1E0";
-}
-.mdi-dribbble-box::before {
- content: "\F1E1";
-}
-.mdi-drone::before {
- content: "\F1E2";
-}
-.mdi-dropbox::before {
- content: "\F1E3";
-}
-.mdi-drupal::before {
- content: "\F1E4";
-}
-.mdi-duck::before {
- content: "\F1E5";
-}
-.mdi-dumbbell::before {
- content: "\F1E6";
-}
-.mdi-dump-truck::before {
- content: "\FC43";
-}
-.mdi-ear-hearing::before {
- content: "\F7C4";
-}
-.mdi-ear-hearing-off::before {
- content: "\FA44";
-}
-.mdi-earth::before {
- content: "\F1E7";
-}
-.mdi-earth-arrow-right::before {
- content: "\F033C";
-}
-.mdi-earth-box::before {
- content: "\F6CC";
-}
-.mdi-earth-box-off::before {
- content: "\F6CD";
-}
-.mdi-earth-off::before {
- content: "\F1E8";
-}
-.mdi-edge::before {
- content: "\F1E9";
-}
-.mdi-edge-legacy::before {
- content: "\F027B";
-}
-.mdi-egg::before {
- content: "\FAAE";
-}
-.mdi-egg-easter::before {
- content: "\FAAF";
-}
-.mdi-eight-track::before {
- content: "\F9E9";
-}
-.mdi-eject::before {
- content: "\F1EA";
-}
-.mdi-eject-outline::before {
- content: "\FB6D";
-}
-.mdi-electric-switch::before {
- content: "\FEBC";
-}
-.mdi-electric-switch-closed::before {
- content: "\F0104";
-}
-.mdi-electron-framework::before {
- content: "\F0046";
-}
-.mdi-elephant::before {
- content: "\F7C5";
-}
-.mdi-elevation-decline::before {
- content: "\F1EB";
-}
-.mdi-elevation-rise::before {
- content: "\F1EC";
-}
-.mdi-elevator::before {
- content: "\F1ED";
-}
-.mdi-elevator-down::before {
- content: "\F02ED";
-}
-.mdi-elevator-passenger::before {
- content: "\F03AC";
-}
-.mdi-elevator-up::before {
- content: "\F02EC";
-}
-.mdi-ellipse::before {
- content: "\FEBD";
-}
-.mdi-ellipse-outline::before {
- content: "\FEBE";
-}
-.mdi-email::before {
- content: "\F1EE";
-}
-.mdi-email-alert::before {
- content: "\F6CE";
-}
-.mdi-email-alert-outline::before {
- content: "\FD1E";
-}
-.mdi-email-box::before {
- content: "\FCDF";
-}
-.mdi-email-check::before {
- content: "\FAB0";
-}
-.mdi-email-check-outline::before {
- content: "\FAB1";
-}
-.mdi-email-edit::before {
- content: "\FF00";
-}
-.mdi-email-edit-outline::before {
- content: "\FF01";
-}
-.mdi-email-lock::before {
- content: "\F1F1";
-}
-.mdi-email-mark-as-unread::before {
- content: "\FB6E";
-}
-.mdi-email-minus::before {
- content: "\FF02";
-}
-.mdi-email-minus-outline::before {
- content: "\FF03";
-}
-.mdi-email-multiple::before {
- content: "\FF04";
-}
-.mdi-email-multiple-outline::before {
- content: "\FF05";
-}
-.mdi-email-newsletter::before {
- content: "\FFD1";
-}
-.mdi-email-open::before {
- content: "\F1EF";
-}
-.mdi-email-open-multiple::before {
- content: "\FF06";
-}
-.mdi-email-open-multiple-outline::before {
- content: "\FF07";
-}
-.mdi-email-open-outline::before {
- content: "\F5EF";
-}
-.mdi-email-outline::before {
- content: "\F1F0";
-}
-.mdi-email-plus::before {
- content: "\F9EA";
-}
-.mdi-email-plus-outline::before {
- content: "\F9EB";
-}
-.mdi-email-receive::before {
- content: "\F0105";
-}
-.mdi-email-receive-outline::before {
- content: "\F0106";
-}
-.mdi-email-search::before {
- content: "\F960";
-}
-.mdi-email-search-outline::before {
- content: "\F961";
-}
-.mdi-email-send::before {
- content: "\F0107";
-}
-.mdi-email-send-outline::before {
- content: "\F0108";
-}
-.mdi-email-sync::before {
- content: "\F02F2";
-}
-.mdi-email-sync-outline::before {
- content: "\F02F3";
-}
-.mdi-email-variant::before {
- content: "\F5F0";
-}
-.mdi-ember::before {
- content: "\FB15";
-}
-.mdi-emby::before {
- content: "\F6B3";
-}
-.mdi-emoticon::before {
- content: "\FC44";
-}
-.mdi-emoticon-angry::before {
- content: "\FC45";
-}
-.mdi-emoticon-angry-outline::before {
- content: "\FC46";
-}
-.mdi-emoticon-confused::before {
- content: "\F0109";
-}
-.mdi-emoticon-confused-outline::before {
- content: "\F010A";
-}
-.mdi-emoticon-cool::before {
- content: "\FC47";
-}
-.mdi-emoticon-cool-outline::before {
- content: "\F1F3";
-}
-.mdi-emoticon-cry::before {
- content: "\FC48";
-}
-.mdi-emoticon-cry-outline::before {
- content: "\FC49";
-}
-.mdi-emoticon-dead::before {
- content: "\FC4A";
-}
-.mdi-emoticon-dead-outline::before {
- content: "\F69A";
-}
-.mdi-emoticon-devil::before {
- content: "\FC4B";
-}
-.mdi-emoticon-devil-outline::before {
- content: "\F1F4";
-}
-.mdi-emoticon-excited::before {
- content: "\FC4C";
-}
-.mdi-emoticon-excited-outline::before {
- content: "\F69B";
-}
-.mdi-emoticon-frown::before {
- content: "\FF69";
-}
-.mdi-emoticon-frown-outline::before {
- content: "\FF6A";
-}
-.mdi-emoticon-happy::before {
- content: "\FC4D";
-}
-.mdi-emoticon-happy-outline::before {
- content: "\F1F5";
-}
-.mdi-emoticon-kiss::before {
- content: "\FC4E";
-}
-.mdi-emoticon-kiss-outline::before {
- content: "\FC4F";
-}
-.mdi-emoticon-lol::before {
- content: "\F023F";
-}
-.mdi-emoticon-lol-outline::before {
- content: "\F0240";
-}
-.mdi-emoticon-neutral::before {
- content: "\FC50";
-}
-.mdi-emoticon-neutral-outline::before {
- content: "\F1F6";
-}
-.mdi-emoticon-outline::before {
- content: "\F1F2";
-}
-.mdi-emoticon-poop::before {
- content: "\F1F7";
-}
-.mdi-emoticon-poop-outline::before {
- content: "\FC51";
-}
-.mdi-emoticon-sad::before {
- content: "\FC52";
-}
-.mdi-emoticon-sad-outline::before {
- content: "\F1F8";
-}
-.mdi-emoticon-tongue::before {
- content: "\F1F9";
-}
-.mdi-emoticon-tongue-outline::before {
- content: "\FC53";
-}
-.mdi-emoticon-wink::before {
- content: "\FC54";
-}
-.mdi-emoticon-wink-outline::before {
- content: "\FC55";
-}
-.mdi-engine::before {
- content: "\F1FA";
-}
-.mdi-engine-off::before {
- content: "\FA45";
-}
-.mdi-engine-off-outline::before {
- content: "\FA46";
-}
-.mdi-engine-outline::before {
- content: "\F1FB";
-}
-.mdi-epsilon::before {
- content: "\F010B";
-}
-.mdi-equal::before {
- content: "\F1FC";
-}
-.mdi-equal-box::before {
- content: "\F1FD";
-}
-.mdi-equalizer::before {
- content: "\FEBF";
-}
-.mdi-equalizer-outline::before {
- content: "\FEC0";
-}
-.mdi-eraser::before {
- content: "\F1FE";
-}
-.mdi-eraser-variant::before {
- content: "\F642";
-}
-.mdi-escalator::before {
- content: "\F1FF";
-}
-.mdi-escalator-down::before {
- content: "\F02EB";
-}
-.mdi-escalator-up::before {
- content: "\F02EA";
-}
-.mdi-eslint::before {
- content: "\FC56";
-}
-.mdi-et::before {
- content: "\FAB2";
-}
-.mdi-ethereum::before {
- content: "\F869";
-}
-.mdi-ethernet::before {
- content: "\F200";
-}
-.mdi-ethernet-cable::before {
- content: "\F201";
-}
-.mdi-ethernet-cable-off::before {
- content: "\F202";
-}
-.mdi-etsy::before {
- content: "\F203";
-}
-.mdi-ev-station::before {
- content: "\F5F1";
-}
-.mdi-eventbrite::before {
- content: "\F7C6";
-}
-.mdi-evernote::before {
- content: "\F204";
-}
-.mdi-excavator::before {
- content: "\F0047";
-}
-.mdi-exclamation::before {
- content: "\F205";
-}
-.mdi-exclamation-thick::before {
- content: "\F0263";
-}
-.mdi-exit-run::before {
- content: "\FA47";
-}
-.mdi-exit-to-app::before {
- content: "\F206";
-}
-.mdi-expand-all::before {
- content: "\FAB3";
-}
-.mdi-expand-all-outline::before {
- content: "\FAB4";
-}
-.mdi-expansion-card::before {
- content: "\F8AD";
-}
-.mdi-expansion-card-variant::before {
- content: "\FFD2";
-}
-.mdi-exponent::before {
- content: "\F962";
-}
-.mdi-exponent-box::before {
- content: "\F963";
-}
-.mdi-export::before {
- content: "\F207";
-}
-.mdi-export-variant::before {
- content: "\FB6F";
-}
-.mdi-eye::before {
- content: "\F208";
-}
-.mdi-eye-check::before {
- content: "\FCE0";
-}
-.mdi-eye-check-outline::before {
- content: "\FCE1";
-}
-.mdi-eye-circle::before {
- content: "\FB70";
-}
-.mdi-eye-circle-outline::before {
- content: "\FB71";
-}
-.mdi-eye-minus::before {
- content: "\F0048";
-}
-.mdi-eye-minus-outline::before {
- content: "\F0049";
-}
-.mdi-eye-off::before {
- content: "\F209";
-}
-.mdi-eye-off-outline::before {
- content: "\F6D0";
-}
-.mdi-eye-outline::before {
- content: "\F6CF";
-}
-.mdi-eye-plus::before {
- content: "\F86A";
-}
-.mdi-eye-plus-outline::before {
- content: "\F86B";
-}
-.mdi-eye-settings::before {
- content: "\F86C";
-}
-.mdi-eye-settings-outline::before {
- content: "\F86D";
-}
-.mdi-eyedropper::before {
- content: "\F20A";
-}
-.mdi-eyedropper-variant::before {
- content: "\F20B";
-}
-.mdi-face::before {
- content: "\F643";
-}
-.mdi-face-agent::before {
- content: "\FD4C";
-}
-.mdi-face-outline::before {
- content: "\FB72";
-}
-.mdi-face-profile::before {
- content: "\F644";
-}
-.mdi-face-profile-woman::before {
- content: "\F00A1";
-}
-.mdi-face-recognition::before {
- content: "\FC57";
-}
-.mdi-face-woman::before {
- content: "\F00A2";
-}
-.mdi-face-woman-outline::before {
- content: "\F00A3";
-}
-.mdi-facebook::before {
- content: "\F20C";
-}
-.mdi-facebook-box::before {
- content: "\F20D";
-}
-.mdi-facebook-messenger::before {
- content: "\F20E";
-}
-.mdi-facebook-workplace::before {
- content: "\FB16";
-}
-.mdi-factory::before {
- content: "\F20F";
-}
-.mdi-fan::before {
- content: "\F210";
-}
-.mdi-fan-off::before {
- content: "\F81C";
-}
-.mdi-fast-forward::before {
- content: "\F211";
-}
-.mdi-fast-forward-10::before {
- content: "\FD4D";
-}
-.mdi-fast-forward-30::before {
- content: "\FCE2";
-}
-.mdi-fast-forward-5::before {
- content: "\F0223";
-}
-.mdi-fast-forward-outline::before {
- content: "\F6D1";
-}
-.mdi-fax::before {
- content: "\F212";
-}
-.mdi-feather::before {
- content: "\F6D2";
-}
-.mdi-feature-search::before {
- content: "\FA48";
-}
-.mdi-feature-search-outline::before {
- content: "\FA49";
-}
-.mdi-fedora::before {
- content: "\F8DA";
-}
-.mdi-ferris-wheel::before {
- content: "\FEC1";
-}
-.mdi-ferry::before {
- content: "\F213";
-}
-.mdi-file::before {
- content: "\F214";
-}
-.mdi-file-account::before {
- content: "\F73A";
-}
-.mdi-file-account-outline::before {
- content: "\F004A";
-}
-.mdi-file-alert::before {
- content: "\FA4A";
-}
-.mdi-file-alert-outline::before {
- content: "\FA4B";
-}
-.mdi-file-cabinet::before {
- content: "\FAB5";
-}
-.mdi-file-cad::before {
- content: "\FF08";
-}
-.mdi-file-cad-box::before {
- content: "\FF09";
-}
-.mdi-file-cancel::before {
- content: "\FDA2";
-}
-.mdi-file-cancel-outline::before {
- content: "\FDA3";
-}
-.mdi-file-certificate::before {
- content: "\F01B1";
-}
-.mdi-file-certificate-outline::before {
- content: "\F01B2";
-}
-.mdi-file-chart::before {
- content: "\F215";
-}
-.mdi-file-chart-outline::before {
- content: "\F004B";
-}
-.mdi-file-check::before {
- content: "\F216";
-}
-.mdi-file-check-outline::before {
- content: "\FE7B";
-}
-.mdi-file-clock::before {
- content: "\F030C";
-}
-.mdi-file-clock-outline::before {
- content: "\F030D";
-}
-.mdi-file-cloud::before {
- content: "\F217";
-}
-.mdi-file-cloud-outline::before {
- content: "\F004C";
-}
-.mdi-file-code::before {
- content: "\F22E";
-}
-.mdi-file-code-outline::before {
- content: "\F004D";
-}
-.mdi-file-compare::before {
- content: "\F8A9";
-}
-.mdi-file-delimited::before {
- content: "\F218";
-}
-.mdi-file-delimited-outline::before {
- content: "\FEC2";
-}
-.mdi-file-document::before {
- content: "\F219";
-}
-.mdi-file-document-box::before {
- content: "\F21A";
-}
-.mdi-file-document-box-check::before {
- content: "\FEC3";
-}
-.mdi-file-document-box-check-outline::before {
- content: "\FEC4";
-}
-.mdi-file-document-box-minus::before {
- content: "\FEC5";
-}
-.mdi-file-document-box-minus-outline::before {
- content: "\FEC6";
-}
-.mdi-file-document-box-multiple::before {
- content: "\FAB6";
-}
-.mdi-file-document-box-multiple-outline::before {
- content: "\FAB7";
-}
-.mdi-file-document-box-outline::before {
- content: "\F9EC";
-}
-.mdi-file-document-box-plus::before {
- content: "\FEC7";
-}
-.mdi-file-document-box-plus-outline::before {
- content: "\FEC8";
-}
-.mdi-file-document-box-remove::before {
- content: "\FEC9";
-}
-.mdi-file-document-box-remove-outline::before {
- content: "\FECA";
-}
-.mdi-file-document-box-search::before {
- content: "\FECB";
-}
-.mdi-file-document-box-search-outline::before {
- content: "\FECC";
-}
-.mdi-file-document-edit::before {
- content: "\FDA4";
-}
-.mdi-file-document-edit-outline::before {
- content: "\FDA5";
-}
-.mdi-file-document-outline::before {
- content: "\F9ED";
-}
-.mdi-file-download::before {
- content: "\F964";
-}
-.mdi-file-download-outline::before {
- content: "\F965";
-}
-.mdi-file-edit::before {
- content: "\F0212";
-}
-.mdi-file-edit-outline::before {
- content: "\F0213";
-}
-.mdi-file-excel::before {
- content: "\F21B";
-}
-.mdi-file-excel-box::before {
- content: "\F21C";
-}
-.mdi-file-excel-box-outline::before {
- content: "\F004E";
-}
-.mdi-file-excel-outline::before {
- content: "\F004F";
-}
-.mdi-file-export::before {
- content: "\F21D";
-}
-.mdi-file-export-outline::before {
- content: "\F0050";
-}
-.mdi-file-eye::before {
- content: "\FDA6";
-}
-.mdi-file-eye-outline::before {
- content: "\FDA7";
-}
-.mdi-file-find::before {
- content: "\F21E";
-}
-.mdi-file-find-outline::before {
- content: "\FB73";
-}
-.mdi-file-hidden::before {
- content: "\F613";
-}
-.mdi-file-image::before {
- content: "\F21F";
-}
-.mdi-file-image-outline::before {
- content: "\FECD";
-}
-.mdi-file-import::before {
- content: "\F220";
-}
-.mdi-file-import-outline::before {
- content: "\F0051";
-}
-.mdi-file-key::before {
- content: "\F01AF";
-}
-.mdi-file-key-outline::before {
- content: "\F01B0";
-}
-.mdi-file-link::before {
- content: "\F01A2";
-}
-.mdi-file-link-outline::before {
- content: "\F01A3";
-}
-.mdi-file-lock::before {
- content: "\F221";
-}
-.mdi-file-lock-outline::before {
- content: "\F0052";
-}
-.mdi-file-move::before {
- content: "\FAB8";
-}
-.mdi-file-move-outline::before {
- content: "\F0053";
-}
-.mdi-file-multiple::before {
- content: "\F222";
-}
-.mdi-file-multiple-outline::before {
- content: "\F0054";
-}
-.mdi-file-music::before {
- content: "\F223";
-}
-.mdi-file-music-outline::before {
- content: "\FE7C";
-}
-.mdi-file-outline::before {
- content: "\F224";
-}
-.mdi-file-pdf::before {
- content: "\F225";
-}
-.mdi-file-pdf-box::before {
- content: "\F226";
-}
-.mdi-file-pdf-box-outline::before {
- content: "\FFD3";
-}
-.mdi-file-pdf-outline::before {
- content: "\FE7D";
-}
-.mdi-file-percent::before {
- content: "\F81D";
-}
-.mdi-file-percent-outline::before {
- content: "\F0055";
-}
-.mdi-file-phone::before {
- content: "\F01A4";
-}
-.mdi-file-phone-outline::before {
- content: "\F01A5";
-}
-.mdi-file-plus::before {
- content: "\F751";
-}
-.mdi-file-plus-outline::before {
- content: "\FF0A";
-}
-.mdi-file-powerpoint::before {
- content: "\F227";
-}
-.mdi-file-powerpoint-box::before {
- content: "\F228";
-}
-.mdi-file-powerpoint-box-outline::before {
- content: "\F0056";
-}
-.mdi-file-powerpoint-outline::before {
- content: "\F0057";
-}
-.mdi-file-presentation-box::before {
- content: "\F229";
-}
-.mdi-file-question::before {
- content: "\F86E";
-}
-.mdi-file-question-outline::before {
- content: "\F0058";
-}
-.mdi-file-remove::before {
- content: "\FB74";
-}
-.mdi-file-remove-outline::before {
- content: "\F0059";
-}
-.mdi-file-replace::before {
- content: "\FB17";
-}
-.mdi-file-replace-outline::before {
- content: "\FB18";
-}
-.mdi-file-restore::before {
- content: "\F670";
-}
-.mdi-file-restore-outline::before {
- content: "\F005A";
-}
-.mdi-file-search::before {
- content: "\FC58";
-}
-.mdi-file-search-outline::before {
- content: "\FC59";
-}
-.mdi-file-send::before {
- content: "\F22A";
-}
-.mdi-file-send-outline::before {
- content: "\F005B";
-}
-.mdi-file-settings::before {
- content: "\F00A4";
-}
-.mdi-file-settings-outline::before {
- content: "\F00A5";
-}
-.mdi-file-settings-variant::before {
- content: "\F00A6";
-}
-.mdi-file-settings-variant-outline::before {
- content: "\F00A7";
-}
-.mdi-file-star::before {
- content: "\F005C";
-}
-.mdi-file-star-outline::before {
- content: "\F005D";
-}
-.mdi-file-swap::before {
- content: "\FFD4";
-}
-.mdi-file-swap-outline::before {
- content: "\FFD5";
-}
-.mdi-file-sync::before {
- content: "\F0241";
-}
-.mdi-file-sync-outline::before {
- content: "\F0242";
-}
-.mdi-file-table::before {
- content: "\FC5A";
-}
-.mdi-file-table-box::before {
- content: "\F010C";
-}
-.mdi-file-table-box-multiple::before {
- content: "\F010D";
-}
-.mdi-file-table-box-multiple-outline::before {
- content: "\F010E";
-}
-.mdi-file-table-box-outline::before {
- content: "\F010F";
-}
-.mdi-file-table-outline::before {
- content: "\FC5B";
-}
-.mdi-file-tree::before {
- content: "\F645";
-}
-.mdi-file-undo::before {
- content: "\F8DB";
-}
-.mdi-file-undo-outline::before {
- content: "\F005E";
-}
-.mdi-file-upload::before {
- content: "\FA4C";
-}
-.mdi-file-upload-outline::before {
- content: "\FA4D";
-}
-.mdi-file-video::before {
- content: "\F22B";
-}
-.mdi-file-video-outline::before {
- content: "\FE10";
-}
-.mdi-file-word::before {
- content: "\F22C";
-}
-.mdi-file-word-box::before {
- content: "\F22D";
-}
-.mdi-file-word-box-outline::before {
- content: "\F005F";
-}
-.mdi-file-word-outline::before {
- content: "\F0060";
-}
-.mdi-film::before {
- content: "\F22F";
-}
-.mdi-filmstrip::before {
- content: "\F230";
-}
-.mdi-filmstrip-off::before {
- content: "\F231";
-}
-.mdi-filter::before {
- content: "\F232";
-}
-.mdi-filter-menu::before {
- content: "\F0110";
-}
-.mdi-filter-menu-outline::before {
- content: "\F0111";
-}
-.mdi-filter-minus::before {
- content: "\FF0B";
-}
-.mdi-filter-minus-outline::before {
- content: "\FF0C";
-}
-.mdi-filter-outline::before {
- content: "\F233";
-}
-.mdi-filter-plus::before {
- content: "\FF0D";
-}
-.mdi-filter-plus-outline::before {
- content: "\FF0E";
-}
-.mdi-filter-remove::before {
- content: "\F234";
-}
-.mdi-filter-remove-outline::before {
- content: "\F235";
-}
-.mdi-filter-variant::before {
- content: "\F236";
-}
-.mdi-filter-variant-minus::before {
- content: "\F013D";
-}
-.mdi-filter-variant-plus::before {
- content: "\F013E";
-}
-.mdi-filter-variant-remove::before {
- content: "\F0061";
-}
-.mdi-finance::before {
- content: "\F81E";
-}
-.mdi-find-replace::before {
- content: "\F6D3";
-}
-.mdi-fingerprint::before {
- content: "\F237";
-}
-.mdi-fingerprint-off::before {
- content: "\FECE";
-}
-.mdi-fire::before {
- content: "\F238";
-}
-.mdi-fire-extinguisher::before {
- content: "\FF0F";
-}
-.mdi-fire-hydrant::before {
- content: "\F0162";
-}
-.mdi-fire-hydrant-alert::before {
- content: "\F0163";
-}
-.mdi-fire-hydrant-off::before {
- content: "\F0164";
-}
-.mdi-fire-truck::before {
- content: "\F8AA";
-}
-.mdi-firebase::before {
- content: "\F966";
-}
-.mdi-firefox::before {
- content: "\F239";
-}
-.mdi-fireplace::before {
- content: "\FE11";
-}
-.mdi-fireplace-off::before {
- content: "\FE12";
-}
-.mdi-firework::before {
- content: "\FE13";
-}
-.mdi-fish::before {
- content: "\F23A";
-}
-.mdi-fishbowl::before {
- content: "\FF10";
-}
-.mdi-fishbowl-outline::before {
- content: "\FF11";
-}
-.mdi-fit-to-page::before {
- content: "\FF12";
-}
-.mdi-fit-to-page-outline::before {
- content: "\FF13";
-}
-.mdi-flag::before {
- content: "\F23B";
-}
-.mdi-flag-checkered::before {
- content: "\F23C";
-}
-.mdi-flag-minus::before {
- content: "\FB75";
-}
-.mdi-flag-minus-outline::before {
- content: "\F00DD";
-}
-.mdi-flag-outline::before {
- content: "\F23D";
-}
-.mdi-flag-plus::before {
- content: "\FB76";
-}
-.mdi-flag-plus-outline::before {
- content: "\F00DE";
-}
-.mdi-flag-remove::before {
- content: "\FB77";
-}
-.mdi-flag-remove-outline::before {
- content: "\F00DF";
-}
-.mdi-flag-triangle::before {
- content: "\F23F";
-}
-.mdi-flag-variant::before {
- content: "\F240";
-}
-.mdi-flag-variant-outline::before {
- content: "\F23E";
-}
-.mdi-flare::before {
- content: "\FD4E";
-}
-.mdi-flash::before {
- content: "\F241";
-}
-.mdi-flash-alert::before {
- content: "\FF14";
-}
-.mdi-flash-alert-outline::before {
- content: "\FF15";
-}
-.mdi-flash-auto::before {
- content: "\F242";
-}
-.mdi-flash-circle::before {
- content: "\F81F";
-}
-.mdi-flash-off::before {
- content: "\F243";
-}
-.mdi-flash-outline::before {
- content: "\F6D4";
-}
-.mdi-flash-red-eye::before {
- content: "\F67A";
-}
-.mdi-flashlight::before {
- content: "\F244";
-}
-.mdi-flashlight-off::before {
- content: "\F245";
-}
-.mdi-flask::before {
- content: "\F093";
-}
-.mdi-flask-empty::before {
- content: "\F094";
-}
-.mdi-flask-empty-minus::before {
- content: "\F0265";
-}
-.mdi-flask-empty-minus-outline::before {
- content: "\F0266";
-}
-.mdi-flask-empty-outline::before {
- content: "\F095";
-}
-.mdi-flask-empty-plus::before {
- content: "\F0267";
-}
-.mdi-flask-empty-plus-outline::before {
- content: "\F0268";
-}
-.mdi-flask-empty-remove::before {
- content: "\F0269";
-}
-.mdi-flask-empty-remove-outline::before {
- content: "\F026A";
-}
-.mdi-flask-minus::before {
- content: "\F026B";
-}
-.mdi-flask-minus-outline::before {
- content: "\F026C";
-}
-.mdi-flask-outline::before {
- content: "\F096";
-}
-.mdi-flask-plus::before {
- content: "\F026D";
-}
-.mdi-flask-plus-outline::before {
- content: "\F026E";
-}
-.mdi-flask-remove::before {
- content: "\F026F";
-}
-.mdi-flask-remove-outline::before {
- content: "\F0270";
-}
-.mdi-flask-round-bottom::before {
- content: "\F0276";
-}
-.mdi-flask-round-bottom-empty::before {
- content: "\F0277";
-}
-.mdi-flask-round-bottom-empty-outline::before {
- content: "\F0278";
-}
-.mdi-flask-round-bottom-outline::before {
- content: "\F0279";
-}
-.mdi-flattr::before {
- content: "\F246";
-}
-.mdi-fleur-de-lis::before {
- content: "\F032E";
-}
-.mdi-flickr::before {
- content: "\FCE3";
-}
-.mdi-flip-horizontal::before {
- content: "\F0112";
-}
-.mdi-flip-to-back::before {
- content: "\F247";
-}
-.mdi-flip-to-front::before {
- content: "\F248";
-}
-.mdi-flip-vertical::before {
- content: "\F0113";
-}
-.mdi-floor-lamp::before {
- content: "\F8DC";
-}
-.mdi-floor-lamp-dual::before {
- content: "\F0062";
-}
-.mdi-floor-lamp-variant::before {
- content: "\F0063";
-}
-.mdi-floor-plan::before {
- content: "\F820";
-}
-.mdi-floppy::before {
- content: "\F249";
-}
-.mdi-floppy-variant::before {
- content: "\F9EE";
-}
-.mdi-flower::before {
- content: "\F24A";
-}
-.mdi-flower-outline::before {
- content: "\F9EF";
-}
-.mdi-flower-poppy::before {
- content: "\FCE4";
-}
-.mdi-flower-tulip::before {
- content: "\F9F0";
-}
-.mdi-flower-tulip-outline::before {
- content: "\F9F1";
-}
-.mdi-focus-auto::before {
- content: "\FF6B";
-}
-.mdi-focus-field::before {
- content: "\FF6C";
-}
-.mdi-focus-field-horizontal::before {
- content: "\FF6D";
-}
-.mdi-focus-field-vertical::before {
- content: "\FF6E";
-}
-.mdi-folder::before {
- content: "\F24B";
-}
-.mdi-folder-account::before {
- content: "\F24C";
-}
-.mdi-folder-account-outline::before {
- content: "\FB78";
-}
-.mdi-folder-alert::before {
- content: "\FDA8";
-}
-.mdi-folder-alert-outline::before {
- content: "\FDA9";
-}
-.mdi-folder-clock::before {
- content: "\FAB9";
-}
-.mdi-folder-clock-outline::before {
- content: "\FABA";
-}
-.mdi-folder-download::before {
- content: "\F24D";
-}
-.mdi-folder-download-outline::before {
- content: "\F0114";
-}
-.mdi-folder-edit::before {
- content: "\F8DD";
-}
-.mdi-folder-edit-outline::before {
- content: "\FDAA";
-}
-.mdi-folder-google-drive::before {
- content: "\F24E";
-}
-.mdi-folder-heart::before {
- content: "\F0115";
-}
-.mdi-folder-heart-outline::before {
- content: "\F0116";
-}
-.mdi-folder-home::before {
- content: "\F00E0";
-}
-.mdi-folder-home-outline::before {
- content: "\F00E1";
-}
-.mdi-folder-image::before {
- content: "\F24F";
-}
-.mdi-folder-information::before {
- content: "\F00E2";
-}
-.mdi-folder-information-outline::before {
- content: "\F00E3";
-}
-.mdi-folder-key::before {
- content: "\F8AB";
-}
-.mdi-folder-key-network::before {
- content: "\F8AC";
-}
-.mdi-folder-key-network-outline::before {
- content: "\FC5C";
-}
-.mdi-folder-key-outline::before {
- content: "\F0117";
-}
-.mdi-folder-lock::before {
- content: "\F250";
-}
-.mdi-folder-lock-open::before {
- content: "\F251";
-}
-.mdi-folder-marker::before {
- content: "\F0298";
-}
-.mdi-folder-marker-outline::before {
- content: "\F0299";
-}
-.mdi-folder-move::before {
- content: "\F252";
-}
-.mdi-folder-move-outline::before {
- content: "\F0271";
-}
-.mdi-folder-multiple::before {
- content: "\F253";
-}
-.mdi-folder-multiple-image::before {
- content: "\F254";
-}
-.mdi-folder-multiple-outline::before {
- content: "\F255";
-}
-.mdi-folder-music::before {
- content: "\F0384";
-}
-.mdi-folder-music-outline::before {
- content: "\F0385";
-}
-.mdi-folder-network::before {
- content: "\F86F";
-}
-.mdi-folder-network-outline::before {
- content: "\FC5D";
-}
-.mdi-folder-open::before {
- content: "\F76F";
-}
-.mdi-folder-open-outline::before {
- content: "\FDAB";
-}
-.mdi-folder-outline::before {
- content: "\F256";
-}
-.mdi-folder-plus::before {
- content: "\F257";
-}
-.mdi-folder-plus-outline::before {
- content: "\FB79";
-}
-.mdi-folder-pound::before {
- content: "\FCE5";
-}
-.mdi-folder-pound-outline::before {
- content: "\FCE6";
-}
-.mdi-folder-remove::before {
- content: "\F258";
-}
-.mdi-folder-remove-outline::before {
- content: "\FB7A";
-}
-.mdi-folder-search::before {
- content: "\F967";
-}
-.mdi-folder-search-outline::before {
- content: "\F968";
-}
-.mdi-folder-settings::before {
- content: "\F00A8";
-}
-.mdi-folder-settings-outline::before {
- content: "\F00A9";
-}
-.mdi-folder-settings-variant::before {
- content: "\F00AA";
-}
-.mdi-folder-settings-variant-outline::before {
- content: "\F00AB";
-}
-.mdi-folder-star::before {
- content: "\F69C";
-}
-.mdi-folder-star-outline::before {
- content: "\FB7B";
-}
-.mdi-folder-swap::before {
- content: "\FFD6";
-}
-.mdi-folder-swap-outline::before {
- content: "\FFD7";
-}
-.mdi-folder-sync::before {
- content: "\FCE7";
-}
-.mdi-folder-sync-outline::before {
- content: "\FCE8";
-}
-.mdi-folder-table::before {
- content: "\F030E";
-}
-.mdi-folder-table-outline::before {
- content: "\F030F";
-}
-.mdi-folder-text::before {
- content: "\FC5E";
-}
-.mdi-folder-text-outline::before {
- content: "\FC5F";
-}
-.mdi-folder-upload::before {
- content: "\F259";
-}
-.mdi-folder-upload-outline::before {
- content: "\F0118";
-}
-.mdi-folder-zip::before {
- content: "\F6EA";
-}
-.mdi-folder-zip-outline::before {
- content: "\F7B8";
-}
-.mdi-font-awesome::before {
- content: "\F03A";
-}
-.mdi-food::before {
- content: "\F25A";
-}
-.mdi-food-apple::before {
- content: "\F25B";
-}
-.mdi-food-apple-outline::before {
- content: "\FC60";
-}
-.mdi-food-croissant::before {
- content: "\F7C7";
-}
-.mdi-food-fork-drink::before {
- content: "\F5F2";
-}
-.mdi-food-off::before {
- content: "\F5F3";
-}
-.mdi-food-variant::before {
- content: "\F25C";
-}
-.mdi-foot-print::before {
- content: "\FF6F";
-}
-.mdi-football::before {
- content: "\F25D";
-}
-.mdi-football-australian::before {
- content: "\F25E";
-}
-.mdi-football-helmet::before {
- content: "\F25F";
-}
-.mdi-forklift::before {
- content: "\F7C8";
-}
-.mdi-format-align-bottom::before {
- content: "\F752";
-}
-.mdi-format-align-center::before {
- content: "\F260";
-}
-.mdi-format-align-justify::before {
- content: "\F261";
-}
-.mdi-format-align-left::before {
- content: "\F262";
-}
-.mdi-format-align-middle::before {
- content: "\F753";
-}
-.mdi-format-align-right::before {
- content: "\F263";
-}
-.mdi-format-align-top::before {
- content: "\F754";
-}
-.mdi-format-annotation-minus::before {
- content: "\FABB";
-}
-.mdi-format-annotation-plus::before {
- content: "\F646";
-}
-.mdi-format-bold::before {
- content: "\F264";
-}
-.mdi-format-clear::before {
- content: "\F265";
-}
-.mdi-format-color-fill::before {
- content: "\F266";
-}
-.mdi-format-color-highlight::before {
- content: "\FE14";
-}
-.mdi-format-color-marker-cancel::before {
- content: "\F033E";
-}
-.mdi-format-color-text::before {
- content: "\F69D";
-}
-.mdi-format-columns::before {
- content: "\F8DE";
-}
-.mdi-format-float-center::before {
- content: "\F267";
-}
-.mdi-format-float-left::before {
- content: "\F268";
-}
-.mdi-format-float-none::before {
- content: "\F269";
-}
-.mdi-format-float-right::before {
- content: "\F26A";
-}
-.mdi-format-font::before {
- content: "\F6D5";
-}
-.mdi-format-font-size-decrease::before {
- content: "\F9F2";
-}
-.mdi-format-font-size-increase::before {
- content: "\F9F3";
-}
-.mdi-format-header-1::before {
- content: "\F26B";
-}
-.mdi-format-header-2::before {
- content: "\F26C";
-}
-.mdi-format-header-3::before {
- content: "\F26D";
-}
-.mdi-format-header-4::before {
- content: "\F26E";
-}
-.mdi-format-header-5::before {
- content: "\F26F";
-}
-.mdi-format-header-6::before {
- content: "\F270";
-}
-.mdi-format-header-decrease::before {
- content: "\F271";
-}
-.mdi-format-header-equal::before {
- content: "\F272";
-}
-.mdi-format-header-increase::before {
- content: "\F273";
-}
-.mdi-format-header-pound::before {
- content: "\F274";
-}
-.mdi-format-horizontal-align-center::before {
- content: "\F61E";
-}
-.mdi-format-horizontal-align-left::before {
- content: "\F61F";
-}
-.mdi-format-horizontal-align-right::before {
- content: "\F620";
-}
-.mdi-format-indent-decrease::before {
- content: "\F275";
-}
-.mdi-format-indent-increase::before {
- content: "\F276";
-}
-.mdi-format-italic::before {
- content: "\F277";
-}
-.mdi-format-letter-case::before {
- content: "\FB19";
-}
-.mdi-format-letter-case-lower::before {
- content: "\FB1A";
-}
-.mdi-format-letter-case-upper::before {
- content: "\FB1B";
-}
-.mdi-format-letter-ends-with::before {
- content: "\FFD8";
-}
-.mdi-format-letter-matches::before {
- content: "\FFD9";
-}
-.mdi-format-letter-starts-with::before {
- content: "\FFDA";
-}
-.mdi-format-line-spacing::before {
- content: "\F278";
-}
-.mdi-format-line-style::before {
- content: "\F5C8";
-}
-.mdi-format-line-weight::before {
- content: "\F5C9";
-}
-.mdi-format-list-bulleted::before {
- content: "\F279";
-}
-.mdi-format-list-bulleted-square::before {
- content: "\FDAC";
-}
-.mdi-format-list-bulleted-triangle::before {
- content: "\FECF";
-}
-.mdi-format-list-bulleted-type::before {
- content: "\F27A";
-}
-.mdi-format-list-checkbox::before {
- content: "\F969";
-}
-.mdi-format-list-checks::before {
- content: "\F755";
-}
-.mdi-format-list-numbered::before {
- content: "\F27B";
-}
-.mdi-format-list-numbered-rtl::before {
- content: "\FCE9";
-}
-.mdi-format-list-text::before {
- content: "\F029A";
-}
-.mdi-format-overline::before {
- content: "\FED0";
-}
-.mdi-format-page-break::before {
- content: "\F6D6";
-}
-.mdi-format-paint::before {
- content: "\F27C";
-}
-.mdi-format-paragraph::before {
- content: "\F27D";
-}
-.mdi-format-pilcrow::before {
- content: "\F6D7";
-}
-.mdi-format-quote-close::before {
- content: "\F27E";
-}
-.mdi-format-quote-close-outline::before {
- content: "\F01D3";
-}
-.mdi-format-quote-open::before {
- content: "\F756";
-}
-.mdi-format-quote-open-outline::before {
- content: "\F01D2";
-}
-.mdi-format-rotate-90::before {
- content: "\F6A9";
-}
-.mdi-format-section::before {
- content: "\F69E";
-}
-.mdi-format-size::before {
- content: "\F27F";
-}
-.mdi-format-strikethrough::before {
- content: "\F280";
-}
-.mdi-format-strikethrough-variant::before {
- content: "\F281";
-}
-.mdi-format-subscript::before {
- content: "\F282";
-}
-.mdi-format-superscript::before {
- content: "\F283";
-}
-.mdi-format-text::before {
- content: "\F284";
-}
-.mdi-format-text-rotation-angle-down::before {
- content: "\FFDB";
-}
-.mdi-format-text-rotation-angle-up::before {
- content: "\FFDC";
-}
-.mdi-format-text-rotation-down::before {
- content: "\FD4F";
-}
-.mdi-format-text-rotation-down-vertical::before {
- content: "\FFDD";
-}
-.mdi-format-text-rotation-none::before {
- content: "\FD50";
-}
-.mdi-format-text-rotation-up::before {
- content: "\FFDE";
-}
-.mdi-format-text-rotation-vertical::before {
- content: "\FFDF";
-}
-.mdi-format-text-variant::before {
- content: "\FE15";
-}
-.mdi-format-text-wrapping-clip::before {
- content: "\FCEA";
-}
-.mdi-format-text-wrapping-overflow::before {
- content: "\FCEB";
-}
-.mdi-format-text-wrapping-wrap::before {
- content: "\FCEC";
-}
-.mdi-format-textbox::before {
- content: "\FCED";
-}
-.mdi-format-textdirection-l-to-r::before {
- content: "\F285";
-}
-.mdi-format-textdirection-r-to-l::before {
- content: "\F286";
-}
-.mdi-format-title::before {
- content: "\F5F4";
-}
-.mdi-format-underline::before {
- content: "\F287";
-}
-.mdi-format-vertical-align-bottom::before {
- content: "\F621";
-}
-.mdi-format-vertical-align-center::before {
- content: "\F622";
-}
-.mdi-format-vertical-align-top::before {
- content: "\F623";
-}
-.mdi-format-wrap-inline::before {
- content: "\F288";
-}
-.mdi-format-wrap-square::before {
- content: "\F289";
-}
-.mdi-format-wrap-tight::before {
- content: "\F28A";
-}
-.mdi-format-wrap-top-bottom::before {
- content: "\F28B";
-}
-.mdi-forum::before {
- content: "\F28C";
-}
-.mdi-forum-outline::before {
- content: "\F821";
-}
-.mdi-forward::before {
- content: "\F28D";
-}
-.mdi-forwardburger::before {
- content: "\FD51";
-}
-.mdi-fountain::before {
- content: "\F96A";
-}
-.mdi-fountain-pen::before {
- content: "\FCEE";
-}
-.mdi-fountain-pen-tip::before {
- content: "\FCEF";
-}
-.mdi-foursquare::before {
- content: "\F28E";
-}
-.mdi-freebsd::before {
- content: "\F8DF";
-}
-.mdi-frequently-asked-questions::before {
- content: "\FED1";
-}
-.mdi-fridge::before {
- content: "\F290";
-}
-.mdi-fridge-alert::before {
- content: "\F01DC";
-}
-.mdi-fridge-alert-outline::before {
- content: "\F01DD";
-}
-.mdi-fridge-bottom::before {
- content: "\F292";
-}
-.mdi-fridge-off::before {
- content: "\F01DA";
-}
-.mdi-fridge-off-outline::before {
- content: "\F01DB";
-}
-.mdi-fridge-outline::before {
- content: "\F28F";
-}
-.mdi-fridge-top::before {
- content: "\F291";
-}
-.mdi-fruit-cherries::before {
- content: "\F0064";
-}
-.mdi-fruit-citrus::before {
- content: "\F0065";
-}
-.mdi-fruit-grapes::before {
- content: "\F0066";
-}
-.mdi-fruit-grapes-outline::before {
- content: "\F0067";
-}
-.mdi-fruit-pineapple::before {
- content: "\F0068";
-}
-.mdi-fruit-watermelon::before {
- content: "\F0069";
-}
-.mdi-fuel::before {
- content: "\F7C9";
-}
-.mdi-fullscreen::before {
- content: "\F293";
-}
-.mdi-fullscreen-exit::before {
- content: "\F294";
-}
-.mdi-function::before {
- content: "\F295";
-}
-.mdi-function-variant::before {
- content: "\F870";
-}
-.mdi-furigana-horizontal::before {
- content: "\F00AC";
-}
-.mdi-furigana-vertical::before {
- content: "\F00AD";
-}
-.mdi-fuse::before {
- content: "\FC61";
-}
-.mdi-fuse-blade::before {
- content: "\FC62";
-}
-.mdi-gamepad::before {
- content: "\F296";
-}
-.mdi-gamepad-circle::before {
- content: "\FE16";
-}
-.mdi-gamepad-circle-down::before {
- content: "\FE17";
-}
-.mdi-gamepad-circle-left::before {
- content: "\FE18";
-}
-.mdi-gamepad-circle-outline::before {
- content: "\FE19";
-}
-.mdi-gamepad-circle-right::before {
- content: "\FE1A";
-}
-.mdi-gamepad-circle-up::before {
- content: "\FE1B";
-}
-.mdi-gamepad-down::before {
- content: "\FE1C";
-}
-.mdi-gamepad-left::before {
- content: "\FE1D";
-}
-.mdi-gamepad-right::before {
- content: "\FE1E";
-}
-.mdi-gamepad-round::before {
- content: "\FE1F";
-}
-.mdi-gamepad-round-down::before {
- content: "\FE7E";
-}
-.mdi-gamepad-round-left::before {
- content: "\FE7F";
-}
-.mdi-gamepad-round-outline::before {
- content: "\FE80";
-}
-.mdi-gamepad-round-right::before {
- content: "\FE81";
-}
-.mdi-gamepad-round-up::before {
- content: "\FE82";
-}
-.mdi-gamepad-square::before {
- content: "\FED2";
-}
-.mdi-gamepad-square-outline::before {
- content: "\FED3";
-}
-.mdi-gamepad-up::before {
- content: "\FE83";
-}
-.mdi-gamepad-variant::before {
- content: "\F297";
-}
-.mdi-gamepad-variant-outline::before {
- content: "\FED4";
-}
-.mdi-gamma::before {
- content: "\F0119";
-}
-.mdi-gantry-crane::before {
- content: "\FDAD";
-}
-.mdi-garage::before {
- content: "\F6D8";
-}
-.mdi-garage-alert::before {
- content: "\F871";
-}
-.mdi-garage-alert-variant::before {
- content: "\F0300";
-}
-.mdi-garage-open::before {
- content: "\F6D9";
-}
-.mdi-garage-open-variant::before {
- content: "\F02FF";
-}
-.mdi-garage-variant::before {
- content: "\F02FE";
-}
-.mdi-gas-cylinder::before {
- content: "\F647";
-}
-.mdi-gas-station::before {
- content: "\F298";
-}
-.mdi-gas-station-outline::before {
- content: "\FED5";
-}
-.mdi-gate::before {
- content: "\F299";
-}
-.mdi-gate-and::before {
- content: "\F8E0";
-}
-.mdi-gate-arrow-right::before {
- content: "\F0194";
-}
-.mdi-gate-nand::before {
- content: "\F8E1";
-}
-.mdi-gate-nor::before {
- content: "\F8E2";
-}
-.mdi-gate-not::before {
- content: "\F8E3";
-}
-.mdi-gate-open::before {
- content: "\F0195";
-}
-.mdi-gate-or::before {
- content: "\F8E4";
-}
-.mdi-gate-xnor::before {
- content: "\F8E5";
-}
-.mdi-gate-xor::before {
- content: "\F8E6";
-}
-.mdi-gatsby::before {
- content: "\FE84";
-}
-.mdi-gauge::before {
- content: "\F29A";
-}
-.mdi-gauge-empty::before {
- content: "\F872";
-}
-.mdi-gauge-full::before {
- content: "\F873";
-}
-.mdi-gauge-low::before {
- content: "\F874";
-}
-.mdi-gavel::before {
- content: "\F29B";
-}
-.mdi-gender-female::before {
- content: "\F29C";
-}
-.mdi-gender-male::before {
- content: "\F29D";
-}
-.mdi-gender-male-female::before {
- content: "\F29E";
-}
-.mdi-gender-male-female-variant::before {
- content: "\F016A";
-}
-.mdi-gender-non-binary::before {
- content: "\F016B";
-}
-.mdi-gender-transgender::before {
- content: "\F29F";
-}
-.mdi-gentoo::before {
- content: "\F8E7";
-}
-.mdi-gesture::before {
- content: "\F7CA";
-}
-.mdi-gesture-double-tap::before {
- content: "\F73B";
-}
-.mdi-gesture-pinch::before {
- content: "\FABC";
-}
-.mdi-gesture-spread::before {
- content: "\FABD";
-}
-.mdi-gesture-swipe::before {
- content: "\FD52";
-}
-.mdi-gesture-swipe-down::before {
- content: "\F73C";
-}
-.mdi-gesture-swipe-horizontal::before {
- content: "\FABE";
-}
-.mdi-gesture-swipe-left::before {
- content: "\F73D";
-}
-.mdi-gesture-swipe-right::before {
- content: "\F73E";
-}
-.mdi-gesture-swipe-up::before {
- content: "\F73F";
-}
-.mdi-gesture-swipe-vertical::before {
- content: "\FABF";
-}
-.mdi-gesture-tap::before {
- content: "\F740";
-}
-.mdi-gesture-tap-box::before {
- content: "\F02D4";
-}
-.mdi-gesture-tap-button::before {
- content: "\F02D3";
-}
-.mdi-gesture-tap-hold::before {
- content: "\FD53";
-}
-.mdi-gesture-two-double-tap::before {
- content: "\F741";
-}
-.mdi-gesture-two-tap::before {
- content: "\F742";
-}
-.mdi-ghost::before {
- content: "\F2A0";
-}
-.mdi-ghost-off::before {
- content: "\F9F4";
-}
-.mdi-gif::before {
- content: "\FD54";
-}
-.mdi-gift::before {
- content: "\FE85";
-}
-.mdi-gift-outline::before {
- content: "\F2A1";
-}
-.mdi-git::before {
- content: "\F2A2";
-}
-.mdi-github-box::before {
- content: "\F2A3";
-}
-.mdi-github-circle::before {
- content: "\F2A4";
-}
-.mdi-github-face::before {
- content: "\F6DA";
-}
-.mdi-gitlab::before {
- content: "\FB7C";
-}
-.mdi-glass-cocktail::before {
- content: "\F356";
-}
-.mdi-glass-flute::before {
- content: "\F2A5";
-}
-.mdi-glass-mug::before {
- content: "\F2A6";
-}
-.mdi-glass-mug-variant::before {
- content: "\F0141";
-}
-.mdi-glass-pint-outline::before {
- content: "\F0338";
-}
-.mdi-glass-stange::before {
- content: "\F2A7";
-}
-.mdi-glass-tulip::before {
- content: "\F2A8";
-}
-.mdi-glass-wine::before {
- content: "\F875";
-}
-.mdi-glassdoor::before {
- content: "\F2A9";
-}
-.mdi-glasses::before {
- content: "\F2AA";
-}
-.mdi-globe-light::before {
- content: "\F0302";
-}
-.mdi-globe-model::before {
- content: "\F8E8";
-}
-.mdi-gmail::before {
- content: "\F2AB";
-}
-.mdi-gnome::before {
- content: "\F2AC";
-}
-.mdi-go-kart::before {
- content: "\FD55";
-}
-.mdi-go-kart-track::before {
- content: "\FD56";
-}
-.mdi-gog::before {
- content: "\FB7D";
-}
-.mdi-gold::before {
- content: "\F027A";
-}
-.mdi-golf::before {
- content: "\F822";
-}
-.mdi-golf-cart::before {
- content: "\F01CF";
-}
-.mdi-golf-tee::before {
- content: "\F00AE";
-}
-.mdi-gondola::before {
- content: "\F685";
-}
-.mdi-goodreads::before {
- content: "\FD57";
-}
-.mdi-google::before {
- content: "\F2AD";
-}
-.mdi-google-adwords::before {
- content: "\FC63";
-}
-.mdi-google-analytics::before {
- content: "\F7CB";
-}
-.mdi-google-assistant::before {
- content: "\F7CC";
-}
-.mdi-google-cardboard::before {
- content: "\F2AE";
-}
-.mdi-google-chrome::before {
- content: "\F2AF";
-}
-.mdi-google-circles::before {
- content: "\F2B0";
-}
-.mdi-google-circles-communities::before {
- content: "\F2B1";
-}
-.mdi-google-circles-extended::before {
- content: "\F2B2";
-}
-.mdi-google-circles-group::before {
- content: "\F2B3";
-}
-.mdi-google-classroom::before {
- content: "\F2C0";
-}
-.mdi-google-cloud::before {
- content: "\F0221";
-}
-.mdi-google-controller::before {
- content: "\F2B4";
-}
-.mdi-google-controller-off::before {
- content: "\F2B5";
-}
-.mdi-google-downasaur::before {
- content: "\F038D";
-}
-.mdi-google-drive::before {
- content: "\F2B6";
-}
-.mdi-google-earth::before {
- content: "\F2B7";
-}
-.mdi-google-fit::before {
- content: "\F96B";
-}
-.mdi-google-glass::before {
- content: "\F2B8";
-}
-.mdi-google-hangouts::before {
- content: "\F2C9";
-}
-.mdi-google-home::before {
- content: "\F823";
-}
-.mdi-google-keep::before {
- content: "\F6DB";
-}
-.mdi-google-lens::before {
- content: "\F9F5";
-}
-.mdi-google-maps::before {
- content: "\F5F5";
-}
-.mdi-google-my-business::before {
- content: "\F006A";
-}
-.mdi-google-nearby::before {
- content: "\F2B9";
-}
-.mdi-google-pages::before {
- content: "\F2BA";
-}
-.mdi-google-photos::before {
- content: "\F6DC";
-}
-.mdi-google-physical-web::before {
- content: "\F2BB";
-}
-.mdi-google-play::before {
- content: "\F2BC";
-}
-.mdi-google-plus::before {
- content: "\F2BD";
-}
-.mdi-google-plus-box::before {
- content: "\F2BE";
-}
-.mdi-google-podcast::before {
- content: "\FED6";
-}
-.mdi-google-spreadsheet::before {
- content: "\F9F6";
-}
-.mdi-google-street-view::before {
- content: "\FC64";
-}
-.mdi-google-translate::before {
- content: "\F2BF";
-}
-.mdi-gradient::before {
- content: "\F69F";
-}
-.mdi-grain::before {
- content: "\FD58";
-}
-.mdi-graph::before {
- content: "\F006B";
-}
-.mdi-graph-outline::before {
- content: "\F006C";
-}
-.mdi-graphql::before {
- content: "\F876";
-}
-.mdi-grave-stone::before {
- content: "\FB7E";
-}
-.mdi-grease-pencil::before {
- content: "\F648";
-}
-.mdi-greater-than::before {
- content: "\F96C";
-}
-.mdi-greater-than-or-equal::before {
- content: "\F96D";
-}
-.mdi-grid::before {
- content: "\F2C1";
-}
-.mdi-grid-large::before {
- content: "\F757";
-}
-.mdi-grid-off::before {
- content: "\F2C2";
-}
-.mdi-grill::before {
- content: "\FE86";
-}
-.mdi-grill-outline::before {
- content: "\F01B5";
-}
-.mdi-group::before {
- content: "\F2C3";
-}
-.mdi-guitar-acoustic::before {
- content: "\F770";
-}
-.mdi-guitar-electric::before {
- content: "\F2C4";
-}
-.mdi-guitar-pick::before {
- content: "\F2C5";
-}
-.mdi-guitar-pick-outline::before {
- content: "\F2C6";
-}
-.mdi-guy-fawkes-mask::before {
- content: "\F824";
-}
-.mdi-hackernews::before {
- content: "\F624";
-}
-.mdi-hail::before {
- content: "\FAC0";
-}
-.mdi-hair-dryer::before {
- content: "\F011A";
-}
-.mdi-hair-dryer-outline::before {
- content: "\F011B";
-}
-.mdi-halloween::before {
- content: "\FB7F";
-}
-.mdi-hamburger::before {
- content: "\F684";
-}
-.mdi-hammer::before {
- content: "\F8E9";
-}
-.mdi-hammer-screwdriver::before {
- content: "\F034D";
-}
-.mdi-hammer-wrench::before {
- content: "\F034E";
-}
-.mdi-hand::before {
- content: "\FA4E";
-}
-.mdi-hand-heart::before {
- content: "\F011C";
-}
-.mdi-hand-left::before {
- content: "\FE87";
-}
-.mdi-hand-okay::before {
- content: "\FA4F";
-}
-.mdi-hand-peace::before {
- content: "\FA50";
-}
-.mdi-hand-peace-variant::before {
- content: "\FA51";
-}
-.mdi-hand-pointing-down::before {
- content: "\FA52";
-}
-.mdi-hand-pointing-left::before {
- content: "\FA53";
-}
-.mdi-hand-pointing-right::before {
- content: "\F2C7";
-}
-.mdi-hand-pointing-up::before {
- content: "\FA54";
-}
-.mdi-hand-right::before {
- content: "\FE88";
-}
-.mdi-hand-saw::before {
- content: "\FE89";
-}
-.mdi-handball::before {
- content: "\FF70";
-}
-.mdi-handcuffs::before {
- content: "\F0169";
-}
-.mdi-handshake::before {
- content: "\F0243";
-}
-.mdi-hanger::before {
- content: "\F2C8";
-}
-.mdi-hard-hat::before {
- content: "\F96E";
-}
-.mdi-harddisk::before {
- content: "\F2CA";
-}
-.mdi-harddisk-plus::before {
- content: "\F006D";
-}
-.mdi-harddisk-remove::before {
- content: "\F006E";
-}
-.mdi-hat-fedora::before {
- content: "\FB80";
-}
-.mdi-hazard-lights::before {
- content: "\FC65";
-}
-.mdi-hdr::before {
- content: "\FD59";
-}
-.mdi-hdr-off::before {
- content: "\FD5A";
-}
-.mdi-head::before {
- content: "\F0389";
-}
-.mdi-head-alert::before {
- content: "\F0363";
-}
-.mdi-head-alert-outline::before {
- content: "\F0364";
-}
-.mdi-head-check::before {
- content: "\F0365";
-}
-.mdi-head-check-outline::before {
- content: "\F0366";
-}
-.mdi-head-cog::before {
- content: "\F0367";
-}
-.mdi-head-cog-outline::before {
- content: "\F0368";
-}
-.mdi-head-dots-horizontal::before {
- content: "\F0369";
-}
-.mdi-head-dots-horizontal-outline::before {
- content: "\F036A";
-}
-.mdi-head-flash::before {
- content: "\F036B";
-}
-.mdi-head-flash-outline::before {
- content: "\F036C";
-}
-.mdi-head-heart::before {
- content: "\F036D";
-}
-.mdi-head-heart-outline::before {
- content: "\F036E";
-}
-.mdi-head-lightbulb::before {
- content: "\F036F";
-}
-.mdi-head-lightbulb-outline::before {
- content: "\F0370";
-}
-.mdi-head-minus::before {
- content: "\F0371";
-}
-.mdi-head-minus-outline::before {
- content: "\F0372";
-}
-.mdi-head-outline::before {
- content: "\F038A";
-}
-.mdi-head-plus::before {
- content: "\F0373";
-}
-.mdi-head-plus-outline::before {
- content: "\F0374";
-}
-.mdi-head-question::before {
- content: "\F0375";
-}
-.mdi-head-question-outline::before {
- content: "\F0376";
-}
-.mdi-head-remove::before {
- content: "\F0377";
-}
-.mdi-head-remove-outline::before {
- content: "\F0378";
-}
-.mdi-head-snowflake::before {
- content: "\F0379";
-}
-.mdi-head-snowflake-outline::before {
- content: "\F037A";
-}
-.mdi-head-sync::before {
- content: "\F037B";
-}
-.mdi-head-sync-outline::before {
- content: "\F037C";
-}
-.mdi-headphones::before {
- content: "\F2CB";
-}
-.mdi-headphones-bluetooth::before {
- content: "\F96F";
-}
-.mdi-headphones-box::before {
- content: "\F2CC";
-}
-.mdi-headphones-off::before {
- content: "\F7CD";
-}
-.mdi-headphones-settings::before {
- content: "\F2CD";
-}
-.mdi-headset::before {
- content: "\F2CE";
-}
-.mdi-headset-dock::before {
- content: "\F2CF";
-}
-.mdi-headset-off::before {
- content: "\F2D0";
-}
-.mdi-heart::before {
- content: "\F2D1";
-}
-.mdi-heart-box::before {
- content: "\F2D2";
-}
-.mdi-heart-box-outline::before {
- content: "\F2D3";
-}
-.mdi-heart-broken::before {
- content: "\F2D4";
-}
-.mdi-heart-broken-outline::before {
- content: "\FCF0";
-}
-.mdi-heart-circle::before {
- content: "\F970";
-}
-.mdi-heart-circle-outline::before {
- content: "\F971";
-}
-.mdi-heart-flash::before {
- content: "\FF16";
-}
-.mdi-heart-half::before {
- content: "\F6DE";
-}
-.mdi-heart-half-full::before {
- content: "\F6DD";
-}
-.mdi-heart-half-outline::before {
- content: "\F6DF";
-}
-.mdi-heart-multiple::before {
- content: "\FA55";
-}
-.mdi-heart-multiple-outline::before {
- content: "\FA56";
-}
-.mdi-heart-off::before {
- content: "\F758";
-}
-.mdi-heart-outline::before {
- content: "\F2D5";
-}
-.mdi-heart-pulse::before {
- content: "\F5F6";
-}
-.mdi-helicopter::before {
- content: "\FAC1";
-}
-.mdi-help::before {
- content: "\F2D6";
-}
-.mdi-help-box::before {
- content: "\F78A";
-}
-.mdi-help-circle::before {
- content: "\F2D7";
-}
-.mdi-help-circle-outline::before {
- content: "\F625";
-}
-.mdi-help-network::before {
- content: "\F6F4";
-}
-.mdi-help-network-outline::before {
- content: "\FC66";
-}
-.mdi-help-rhombus::before {
- content: "\FB81";
-}
-.mdi-help-rhombus-outline::before {
- content: "\FB82";
-}
-.mdi-hexadecimal::before {
- content: "\F02D2";
-}
-.mdi-hexagon::before {
- content: "\F2D8";
-}
-.mdi-hexagon-multiple::before {
- content: "\F6E0";
-}
-.mdi-hexagon-multiple-outline::before {
- content: "\F011D";
-}
-.mdi-hexagon-outline::before {
- content: "\F2D9";
-}
-.mdi-hexagon-slice-1::before {
- content: "\FAC2";
-}
-.mdi-hexagon-slice-2::before {
- content: "\FAC3";
-}
-.mdi-hexagon-slice-3::before {
- content: "\FAC4";
-}
-.mdi-hexagon-slice-4::before {
- content: "\FAC5";
-}
-.mdi-hexagon-slice-5::before {
- content: "\FAC6";
-}
-.mdi-hexagon-slice-6::before {
- content: "\FAC7";
-}
-.mdi-hexagram::before {
- content: "\FAC8";
-}
-.mdi-hexagram-outline::before {
- content: "\FAC9";
-}
-.mdi-high-definition::before {
- content: "\F7CE";
-}
-.mdi-high-definition-box::before {
- content: "\F877";
-}
-.mdi-highway::before {
- content: "\F5F7";
-}
-.mdi-hiking::before {
- content: "\FD5B";
-}
-.mdi-hinduism::before {
- content: "\F972";
-}
-.mdi-history::before {
- content: "\F2DA";
-}
-.mdi-hockey-puck::before {
- content: "\F878";
-}
-.mdi-hockey-sticks::before {
- content: "\F879";
-}
-.mdi-hololens::before {
- content: "\F2DB";
-}
-.mdi-home::before {
- content: "\F2DC";
-}
-.mdi-home-account::before {
- content: "\F825";
-}
-.mdi-home-alert::before {
- content: "\F87A";
-}
-.mdi-home-analytics::before {
- content: "\FED7";
-}
-.mdi-home-assistant::before {
- content: "\F7CF";
-}
-.mdi-home-automation::before {
- content: "\F7D0";
-}
-.mdi-home-circle::before {
- content: "\F7D1";
-}
-.mdi-home-circle-outline::before {
- content: "\F006F";
-}
-.mdi-home-city::before {
- content: "\FCF1";
-}
-.mdi-home-city-outline::before {
- content: "\FCF2";
-}
-.mdi-home-currency-usd::before {
- content: "\F8AE";
-}
-.mdi-home-edit::before {
- content: "\F0184";
-}
-.mdi-home-edit-outline::before {
- content: "\F0185";
-}
-.mdi-home-export-outline::before {
- content: "\FFB8";
-}
-.mdi-home-flood::before {
- content: "\FF17";
-}
-.mdi-home-floor-0::before {
- content: "\FDAE";
-}
-.mdi-home-floor-1::before {
- content: "\FD5C";
-}
-.mdi-home-floor-2::before {
- content: "\FD5D";
-}
-.mdi-home-floor-3::before {
- content: "\FD5E";
-}
-.mdi-home-floor-a::before {
- content: "\FD5F";
-}
-.mdi-home-floor-b::before {
- content: "\FD60";
-}
-.mdi-home-floor-g::before {
- content: "\FD61";
-}
-.mdi-home-floor-l::before {
- content: "\FD62";
-}
-.mdi-home-floor-negative-1::before {
- content: "\FDAF";
-}
-.mdi-home-group::before {
- content: "\FDB0";
-}
-.mdi-home-heart::before {
- content: "\F826";
-}
-.mdi-home-import-outline::before {
- content: "\FFB9";
-}
-.mdi-home-lightbulb::before {
- content: "\F027C";
-}
-.mdi-home-lightbulb-outline::before {
- content: "\F027D";
-}
-.mdi-home-lock::before {
- content: "\F8EA";
-}
-.mdi-home-lock-open::before {
- content: "\F8EB";
-}
-.mdi-home-map-marker::before {
- content: "\F5F8";
-}
-.mdi-home-minus::before {
- content: "\F973";
-}
-.mdi-home-modern::before {
- content: "\F2DD";
-}
-.mdi-home-outline::before {
- content: "\F6A0";
-}
-.mdi-home-plus::before {
- content: "\F974";
-}
-.mdi-home-remove::before {
- content: "\F0272";
-}
-.mdi-home-roof::before {
- content: "\F0156";
-}
-.mdi-home-thermometer::before {
- content: "\FF71";
-}
-.mdi-home-thermometer-outline::before {
- content: "\FF72";
-}
-.mdi-home-variant::before {
- content: "\F2DE";
-}
-.mdi-home-variant-outline::before {
- content: "\FB83";
-}
-.mdi-hook::before {
- content: "\F6E1";
-}
-.mdi-hook-off::before {
- content: "\F6E2";
-}
-.mdi-hops::before {
- content: "\F2DF";
-}
-.mdi-horizontal-rotate-clockwise::before {
- content: "\F011E";
-}
-.mdi-horizontal-rotate-counterclockwise::before {
- content: "\F011F";
-}
-.mdi-horseshoe::before {
- content: "\FA57";
-}
-.mdi-hospital::before {
- content: "\F0017";
-}
-.mdi-hospital-box::before {
- content: "\F2E0";
-}
-.mdi-hospital-box-outline::before {
- content: "\F0018";
-}
-.mdi-hospital-building::before {
- content: "\F2E1";
-}
-.mdi-hospital-marker::before {
- content: "\F2E2";
-}
-.mdi-hot-tub::before {
- content: "\F827";
-}
-.mdi-hotel::before {
- content: "\F2E3";
-}
-.mdi-houzz::before {
- content: "\F2E4";
-}
-.mdi-houzz-box::before {
- content: "\F2E5";
-}
-.mdi-hubspot::before {
- content: "\FCF3";
-}
-.mdi-hulu::before {
- content: "\F828";
-}
-.mdi-human::before {
- content: "\F2E6";
-}
-.mdi-human-child::before {
- content: "\F2E7";
-}
-.mdi-human-female::before {
- content: "\F649";
-}
-.mdi-human-female-boy::before {
- content: "\FA58";
-}
-.mdi-human-female-female::before {
- content: "\FA59";
-}
-.mdi-human-female-girl::before {
- content: "\FA5A";
-}
-.mdi-human-greeting::before {
- content: "\F64A";
-}
-.mdi-human-handsdown::before {
- content: "\F64B";
-}
-.mdi-human-handsup::before {
- content: "\F64C";
-}
-.mdi-human-male::before {
- content: "\F64D";
-}
-.mdi-human-male-boy::before {
- content: "\FA5B";
-}
-.mdi-human-male-female::before {
- content: "\F2E8";
-}
-.mdi-human-male-girl::before {
- content: "\FA5C";
-}
-.mdi-human-male-height::before {
- content: "\FF18";
-}
-.mdi-human-male-height-variant::before {
- content: "\FF19";
-}
-.mdi-human-male-male::before {
- content: "\FA5D";
-}
-.mdi-human-pregnant::before {
- content: "\F5CF";
-}
-.mdi-humble-bundle::before {
- content: "\F743";
-}
-.mdi-hvac::before {
- content: "\F037D";
-}
-.mdi-hydraulic-oil-level::before {
- content: "\F034F";
-}
-.mdi-hydraulic-oil-temperature::before {
- content: "\F0350";
-}
-.mdi-hydro-power::before {
- content: "\F0310";
-}
-.mdi-ice-cream::before {
- content: "\F829";
-}
-.mdi-ice-pop::before {
- content: "\FF1A";
-}
-.mdi-id-card::before {
- content: "\FFE0";
-}
-.mdi-identifier::before {
- content: "\FF1B";
-}
-.mdi-ideogram-cjk::before {
- content: "\F035C";
-}
-.mdi-ideogram-cjk-variant::before {
- content: "\F035D";
-}
-.mdi-iframe::before {
- content: "\FC67";
-}
-.mdi-iframe-array::before {
- content: "\F0120";
-}
-.mdi-iframe-array-outline::before {
- content: "\F0121";
-}
-.mdi-iframe-braces::before {
- content: "\F0122";
-}
-.mdi-iframe-braces-outline::before {
- content: "\F0123";
-}
-.mdi-iframe-outline::before {
- content: "\FC68";
-}
-.mdi-iframe-parentheses::before {
- content: "\F0124";
-}
-.mdi-iframe-parentheses-outline::before {
- content: "\F0125";
-}
-.mdi-iframe-variable::before {
- content: "\F0126";
-}
-.mdi-iframe-variable-outline::before {
- content: "\F0127";
-}
-.mdi-image::before {
- content: "\F2E9";
-}
-.mdi-image-album::before {
- content: "\F2EA";
-}
-.mdi-image-area::before {
- content: "\F2EB";
-}
-.mdi-image-area-close::before {
- content: "\F2EC";
-}
-.mdi-image-auto-adjust::before {
- content: "\FFE1";
-}
-.mdi-image-broken::before {
- content: "\F2ED";
-}
-.mdi-image-broken-variant::before {
- content: "\F2EE";
-}
-.mdi-image-edit::before {
- content: "\F020E";
-}
-.mdi-image-edit-outline::before {
- content: "\F020F";
-}
-.mdi-image-filter::before {
- content: "\F2EF";
-}
-.mdi-image-filter-black-white::before {
- content: "\F2F0";
-}
-.mdi-image-filter-center-focus::before {
- content: "\F2F1";
-}
-.mdi-image-filter-center-focus-strong::before {
- content: "\FF1C";
-}
-.mdi-image-filter-center-focus-strong-outline::before {
- content: "\FF1D";
-}
-.mdi-image-filter-center-focus-weak::before {
- content: "\F2F2";
-}
-.mdi-image-filter-drama::before {
- content: "\F2F3";
-}
-.mdi-image-filter-frames::before {
- content: "\F2F4";
-}
-.mdi-image-filter-hdr::before {
- content: "\F2F5";
-}
-.mdi-image-filter-none::before {
- content: "\F2F6";
-}
-.mdi-image-filter-tilt-shift::before {
- content: "\F2F7";
-}
-.mdi-image-filter-vintage::before {
- content: "\F2F8";
-}
-.mdi-image-frame::before {
- content: "\FE8A";
-}
-.mdi-image-move::before {
- content: "\F9F7";
-}
-.mdi-image-multiple::before {
- content: "\F2F9";
-}
-.mdi-image-off::before {
- content: "\F82A";
-}
-.mdi-image-off-outline::before {
- content: "\F01FC";
-}
-.mdi-image-outline::before {
- content: "\F975";
-}
-.mdi-image-plus::before {
- content: "\F87B";
-}
-.mdi-image-search::before {
- content: "\F976";
-}
-.mdi-image-search-outline::before {
- content: "\F977";
-}
-.mdi-image-size-select-actual::before {
- content: "\FC69";
-}
-.mdi-image-size-select-large::before {
- content: "\FC6A";
-}
-.mdi-image-size-select-small::before {
- content: "\FC6B";
-}
-.mdi-import::before {
- content: "\F2FA";
-}
-.mdi-inbox::before {
- content: "\F686";
-}
-.mdi-inbox-arrow-down::before {
- content: "\F2FB";
-}
-.mdi-inbox-arrow-down-outline::before {
- content: "\F029B";
-}
-.mdi-inbox-arrow-up::before {
- content: "\F3D1";
-}
-.mdi-inbox-arrow-up-outline::before {
- content: "\F029C";
-}
-.mdi-inbox-full::before {
- content: "\F029D";
-}
-.mdi-inbox-full-outline::before {
- content: "\F029E";
-}
-.mdi-inbox-multiple::before {
- content: "\F8AF";
-}
-.mdi-inbox-multiple-outline::before {
- content: "\FB84";
-}
-.mdi-inbox-outline::before {
- content: "\F029F";
-}
-.mdi-incognito::before {
- content: "\F5F9";
-}
-.mdi-infinity::before {
- content: "\F6E3";
-}
-.mdi-information::before {
- content: "\F2FC";
-}
-.mdi-information-outline::before {
- content: "\F2FD";
-}
-.mdi-information-variant::before {
- content: "\F64E";
-}
-.mdi-instagram::before {
- content: "\F2FE";
-}
-.mdi-instapaper::before {
- content: "\F2FF";
-}
-.mdi-instrument-triangle::before {
- content: "\F0070";
-}
-.mdi-internet-explorer::before {
- content: "\F300";
-}
-.mdi-invert-colors::before {
- content: "\F301";
-}
-.mdi-invert-colors-off::before {
- content: "\FE8B";
-}
-.mdi-iobroker::before {
- content: "\F0313";
-}
-.mdi-ip::before {
- content: "\FA5E";
-}
-.mdi-ip-network::before {
- content: "\FA5F";
-}
-.mdi-ip-network-outline::before {
- content: "\FC6C";
-}
-.mdi-ipod::before {
- content: "\FC6D";
-}
-.mdi-islam::before {
- content: "\F978";
-}
-.mdi-island::before {
- content: "\F0071";
-}
-.mdi-itunes::before {
- content: "\F676";
-}
-.mdi-iv-bag::before {
- content: "\F00E4";
-}
-.mdi-jabber::before {
- content: "\FDB1";
-}
-.mdi-jeepney::before {
- content: "\F302";
-}
-.mdi-jellyfish::before {
- content: "\FF1E";
-}
-.mdi-jellyfish-outline::before {
- content: "\FF1F";
-}
-.mdi-jira::before {
- content: "\F303";
-}
-.mdi-jquery::before {
- content: "\F87C";
-}
-.mdi-jsfiddle::before {
- content: "\F304";
-}
-.mdi-json::before {
- content: "\F626";
-}
-.mdi-judaism::before {
- content: "\F979";
-}
-.mdi-jump-rope::before {
- content: "\F032A";
-}
-.mdi-kabaddi::before {
- content: "\FD63";
-}
-.mdi-karate::before {
- content: "\F82B";
-}
-.mdi-keg::before {
- content: "\F305";
-}
-.mdi-kettle::before {
- content: "\F5FA";
-}
-.mdi-kettle-alert::before {
- content: "\F0342";
-}
-.mdi-kettle-alert-outline::before {
- content: "\F0343";
-}
-.mdi-kettle-off::before {
- content: "\F0346";
-}
-.mdi-kettle-off-outline::before {
- content: "\F0347";
-}
-.mdi-kettle-outline::before {
- content: "\FF73";
-}
-.mdi-kettle-steam::before {
- content: "\F0344";
-}
-.mdi-kettle-steam-outline::before {
- content: "\F0345";
-}
-.mdi-kettlebell::before {
- content: "\F032B";
-}
-.mdi-key::before {
- content: "\F306";
-}
-.mdi-key-arrow-right::before {
- content: "\F033D";
-}
-.mdi-key-change::before {
- content: "\F307";
-}
-.mdi-key-link::before {
- content: "\F01CA";
-}
-.mdi-key-minus::before {
- content: "\F308";
-}
-.mdi-key-outline::before {
- content: "\FDB2";
-}
-.mdi-key-plus::before {
- content: "\F309";
-}
-.mdi-key-remove::before {
- content: "\F30A";
-}
-.mdi-key-star::before {
- content: "\F01C9";
-}
-.mdi-key-variant::before {
- content: "\F30B";
-}
-.mdi-key-wireless::before {
- content: "\FFE2";
-}
-.mdi-keyboard::before {
- content: "\F30C";
-}
-.mdi-keyboard-backspace::before {
- content: "\F30D";
-}
-.mdi-keyboard-caps::before {
- content: "\F30E";
-}
-.mdi-keyboard-close::before {
- content: "\F30F";
-}
-.mdi-keyboard-esc::before {
- content: "\F02E2";
-}
-.mdi-keyboard-f1::before {
- content: "\F02D6";
-}
-.mdi-keyboard-f10::before {
- content: "\F02DF";
-}
-.mdi-keyboard-f11::before {
- content: "\F02E0";
-}
-.mdi-keyboard-f12::before {
- content: "\F02E1";
-}
-.mdi-keyboard-f2::before {
- content: "\F02D7";
-}
-.mdi-keyboard-f3::before {
- content: "\F02D8";
-}
-.mdi-keyboard-f4::before {
- content: "\F02D9";
-}
-.mdi-keyboard-f5::before {
- content: "\F02DA";
-}
-.mdi-keyboard-f6::before {
- content: "\F02DB";
-}
-.mdi-keyboard-f7::before {
- content: "\F02DC";
-}
-.mdi-keyboard-f8::before {
- content: "\F02DD";
-}
-.mdi-keyboard-f9::before {
- content: "\F02DE";
-}
-.mdi-keyboard-off::before {
- content: "\F310";
-}
-.mdi-keyboard-off-outline::before {
- content: "\FE8C";
-}
-.mdi-keyboard-outline::before {
- content: "\F97A";
-}
-.mdi-keyboard-return::before {
- content: "\F311";
-}
-.mdi-keyboard-settings::before {
- content: "\F9F8";
-}
-.mdi-keyboard-settings-outline::before {
- content: "\F9F9";
-}
-.mdi-keyboard-space::before {
- content: "\F0072";
-}
-.mdi-keyboard-tab::before {
- content: "\F312";
-}
-.mdi-keyboard-variant::before {
- content: "\F313";
-}
-.mdi-khanda::before {
- content: "\F0128";
-}
-.mdi-kickstarter::before {
- content: "\F744";
-}
-.mdi-klingon::before {
- content: "\F0386";
-}
-.mdi-knife::before {
- content: "\F9FA";
-}
-.mdi-knife-military::before {
- content: "\F9FB";
-}
-.mdi-kodi::before {
- content: "\F314";
-}
-.mdi-kotlin::before {
- content: "\F0244";
-}
-.mdi-kubernetes::before {
- content: "\F0129";
-}
-.mdi-label::before {
- content: "\F315";
-}
-.mdi-label-multiple::before {
- content: "\F03A0";
-}
-.mdi-label-multiple-outline::before {
- content: "\F03A1";
-}
-.mdi-label-off::before {
- content: "\FACA";
-}
-.mdi-label-off-outline::before {
- content: "\FACB";
-}
-.mdi-label-outline::before {
- content: "\F316";
-}
-.mdi-label-percent::before {
- content: "\F0315";
-}
-.mdi-label-percent-outline::before {
- content: "\F0316";
-}
-.mdi-label-variant::before {
- content: "\FACC";
-}
-.mdi-label-variant-outline::before {
- content: "\FACD";
-}
-.mdi-ladybug::before {
- content: "\F82C";
-}
-.mdi-lambda::before {
- content: "\F627";
-}
-.mdi-lamp::before {
- content: "\F6B4";
-}
-.mdi-lan::before {
- content: "\F317";
-}
-.mdi-lan-check::before {
- content: "\F02D5";
-}
-.mdi-lan-connect::before {
- content: "\F318";
-}
-.mdi-lan-disconnect::before {
- content: "\F319";
-}
-.mdi-lan-pending::before {
- content: "\F31A";
-}
-.mdi-language-c::before {
- content: "\F671";
-}
-.mdi-language-cpp::before {
- content: "\F672";
-}
-.mdi-language-csharp::before {
- content: "\F31B";
-}
-.mdi-language-css3::before {
- content: "\F31C";
-}
-.mdi-language-fortran::before {
- content: "\F0245";
-}
-.mdi-language-go::before {
- content: "\F7D2";
-}
-.mdi-language-haskell::before {
- content: "\FC6E";
-}
-.mdi-language-html5::before {
- content: "\F31D";
-}
-.mdi-language-java::before {
- content: "\FB1C";
-}
-.mdi-language-javascript::before {
- content: "\F31E";
-}
-.mdi-language-lua::before {
- content: "\F8B0";
-}
-.mdi-language-php::before {
- content: "\F31F";
-}
-.mdi-language-python::before {
- content: "\F320";
-}
-.mdi-language-python-text::before {
- content: "\F321";
-}
-.mdi-language-r::before {
- content: "\F7D3";
-}
-.mdi-language-ruby-on-rails::before {
- content: "\FACE";
-}
-.mdi-language-swift::before {
- content: "\F6E4";
-}
-.mdi-language-typescript::before {
- content: "\F6E5";
-}
-.mdi-laptop::before {
- content: "\F322";
-}
-.mdi-laptop-chromebook::before {
- content: "\F323";
-}
-.mdi-laptop-mac::before {
- content: "\F324";
-}
-.mdi-laptop-off::before {
- content: "\F6E6";
-}
-.mdi-laptop-windows::before {
- content: "\F325";
-}
-.mdi-laravel::before {
- content: "\FACF";
-}
-.mdi-lasso::before {
- content: "\FF20";
-}
-.mdi-lastfm::before {
- content: "\F326";
-}
-.mdi-lastpass::before {
- content: "\F446";
-}
-.mdi-latitude::before {
- content: "\FF74";
-}
-.mdi-launch::before {
- content: "\F327";
-}
-.mdi-lava-lamp::before {
- content: "\F7D4";
-}
-.mdi-layers::before {
- content: "\F328";
-}
-.mdi-layers-minus::before {
- content: "\FE8D";
-}
-.mdi-layers-off::before {
- content: "\F329";
-}
-.mdi-layers-off-outline::before {
- content: "\F9FC";
-}
-.mdi-layers-outline::before {
- content: "\F9FD";
-}
-.mdi-layers-plus::before {
- content: "\FE30";
-}
-.mdi-layers-remove::before {
- content: "\FE31";
-}
-.mdi-layers-search::before {
- content: "\F0231";
-}
-.mdi-layers-search-outline::before {
- content: "\F0232";
-}
-.mdi-layers-triple::before {
- content: "\FF75";
-}
-.mdi-layers-triple-outline::before {
- content: "\FF76";
-}
-.mdi-lead-pencil::before {
- content: "\F64F";
-}
-.mdi-leaf::before {
- content: "\F32A";
-}
-.mdi-leaf-maple::before {
- content: "\FC6F";
-}
-.mdi-leaf-maple-off::before {
- content: "\F0305";
-}
-.mdi-leaf-off::before {
- content: "\F0304";
-}
-.mdi-leak::before {
- content: "\FDB3";
-}
-.mdi-leak-off::before {
- content: "\FDB4";
-}
-.mdi-led-off::before {
- content: "\F32B";
-}
-.mdi-led-on::before {
- content: "\F32C";
-}
-.mdi-led-outline::before {
- content: "\F32D";
-}
-.mdi-led-strip::before {
- content: "\F7D5";
-}
-.mdi-led-strip-variant::before {
- content: "\F0073";
-}
-.mdi-led-variant-off::before {
- content: "\F32E";
-}
-.mdi-led-variant-on::before {
- content: "\F32F";
-}
-.mdi-led-variant-outline::before {
- content: "\F330";
-}
-.mdi-leek::before {
- content: "\F01A8";
-}
-.mdi-less-than::before {
- content: "\F97B";
-}
-.mdi-less-than-or-equal::before {
- content: "\F97C";
-}
-.mdi-library::before {
- content: "\F331";
-}
-.mdi-library-books::before {
- content: "\F332";
-}
-.mdi-library-movie::before {
- content: "\FCF4";
-}
-.mdi-library-music::before {
- content: "\F333";
-}
-.mdi-library-music-outline::before {
- content: "\FF21";
-}
-.mdi-library-shelves::before {
- content: "\FB85";
-}
-.mdi-library-video::before {
- content: "\FCF5";
-}
-.mdi-license::before {
- content: "\FFE3";
-}
-.mdi-lifebuoy::before {
- content: "\F87D";
-}
-.mdi-light-switch::before {
- content: "\F97D";
-}
-.mdi-lightbulb::before {
- content: "\F335";
-}
-.mdi-lightbulb-cfl::before {
- content: "\F0233";
-}
-.mdi-lightbulb-cfl-off::before {
- content: "\F0234";
-}
-.mdi-lightbulb-cfl-spiral::before {
- content: "\F02A0";
-}
-.mdi-lightbulb-cfl-spiral-off::before {
- content: "\F02EE";
-}
-.mdi-lightbulb-group::before {
- content: "\F027E";
-}
-.mdi-lightbulb-group-off::before {
- content: "\F02F8";
-}
-.mdi-lightbulb-group-off-outline::before {
- content: "\F02F9";
-}
-.mdi-lightbulb-group-outline::before {
- content: "\F027F";
-}
-.mdi-lightbulb-multiple::before {
- content: "\F0280";
-}
-.mdi-lightbulb-multiple-off::before {
- content: "\F02FA";
-}
-.mdi-lightbulb-multiple-off-outline::before {
- content: "\F02FB";
-}
-.mdi-lightbulb-multiple-outline::before {
- content: "\F0281";
-}
-.mdi-lightbulb-off::before {
- content: "\FE32";
-}
-.mdi-lightbulb-off-outline::before {
- content: "\FE33";
-}
-.mdi-lightbulb-on::before {
- content: "\F6E7";
-}
-.mdi-lightbulb-on-outline::before {
- content: "\F6E8";
-}
-.mdi-lightbulb-outline::before {
- content: "\F336";
-}
-.mdi-lighthouse::before {
- content: "\F9FE";
-}
-.mdi-lighthouse-on::before {
- content: "\F9FF";
-}
-.mdi-link::before {
- content: "\F337";
-}
-.mdi-link-box::before {
- content: "\FCF6";
-}
-.mdi-link-box-outline::before {
- content: "\FCF7";
-}
-.mdi-link-box-variant::before {
- content: "\FCF8";
-}
-.mdi-link-box-variant-outline::before {
- content: "\FCF9";
-}
-.mdi-link-lock::before {
- content: "\F00E5";
-}
-.mdi-link-off::before {
- content: "\F338";
-}
-.mdi-link-plus::before {
- content: "\FC70";
-}
-.mdi-link-variant::before {
- content: "\F339";
-}
-.mdi-link-variant-minus::before {
- content: "\F012A";
-}
-.mdi-link-variant-off::before {
- content: "\F33A";
-}
-.mdi-link-variant-plus::before {
- content: "\F012B";
-}
-.mdi-link-variant-remove::before {
- content: "\F012C";
-}
-.mdi-linkedin::before {
- content: "\F33B";
-}
-.mdi-linkedin-box::before {
- content: "\F33C";
-}
-.mdi-linux::before {
- content: "\F33D";
-}
-.mdi-linux-mint::before {
- content: "\F8EC";
-}
-.mdi-litecoin::before {
- content: "\FA60";
-}
-.mdi-loading::before {
- content: "\F771";
-}
-.mdi-location-enter::before {
- content: "\FFE4";
-}
-.mdi-location-exit::before {
- content: "\FFE5";
-}
-.mdi-lock::before {
- content: "\F33E";
-}
-.mdi-lock-alert::before {
- content: "\F8ED";
-}
-.mdi-lock-clock::before {
- content: "\F97E";
-}
-.mdi-lock-open::before {
- content: "\F33F";
-}
-.mdi-lock-open-outline::before {
- content: "\F340";
-}
-.mdi-lock-open-variant::before {
- content: "\FFE6";
-}
-.mdi-lock-open-variant-outline::before {
- content: "\FFE7";
-}
-.mdi-lock-outline::before {
- content: "\F341";
-}
-.mdi-lock-pattern::before {
- content: "\F6E9";
-}
-.mdi-lock-plus::before {
- content: "\F5FB";
-}
-.mdi-lock-question::before {
- content: "\F8EE";
-}
-.mdi-lock-reset::before {
- content: "\F772";
-}
-.mdi-lock-smart::before {
- content: "\F8B1";
-}
-.mdi-locker::before {
- content: "\F7D6";
-}
-.mdi-locker-multiple::before {
- content: "\F7D7";
-}
-.mdi-login::before {
- content: "\F342";
-}
-.mdi-login-variant::before {
- content: "\F5FC";
-}
-.mdi-logout::before {
- content: "\F343";
-}
-.mdi-logout-variant::before {
- content: "\F5FD";
-}
-.mdi-longitude::before {
- content: "\FF77";
-}
-.mdi-looks::before {
- content: "\F344";
-}
-.mdi-loupe::before {
- content: "\F345";
-}
-.mdi-lumx::before {
- content: "\F346";
-}
-.mdi-lungs::before {
- content: "\F00AF";
-}
-.mdi-lyft::before {
- content: "\FB1D";
-}
-.mdi-magnet::before {
- content: "\F347";
-}
-.mdi-magnet-on::before {
- content: "\F348";
-}
-.mdi-magnify::before {
- content: "\F349";
-}
-.mdi-magnify-close::before {
- content: "\F97F";
-}
-.mdi-magnify-minus::before {
- content: "\F34A";
-}
-.mdi-magnify-minus-cursor::before {
- content: "\FA61";
-}
-.mdi-magnify-minus-outline::before {
- content: "\F6EB";
-}
-.mdi-magnify-plus::before {
- content: "\F34B";
-}
-.mdi-magnify-plus-cursor::before {
- content: "\FA62";
-}
-.mdi-magnify-plus-outline::before {
- content: "\F6EC";
-}
-.mdi-magnify-remove-cursor::before {
- content: "\F0237";
-}
-.mdi-magnify-remove-outline::before {
- content: "\F0238";
-}
-.mdi-magnify-scan::before {
- content: "\F02A1";
-}
-.mdi-mail::before {
- content: "\FED8";
-}
-.mdi-mail-ru::before {
- content: "\F34C";
-}
-.mdi-mailbox::before {
- content: "\F6ED";
-}
-.mdi-mailbox-open::before {
- content: "\FD64";
-}
-.mdi-mailbox-open-outline::before {
- content: "\FD65";
-}
-.mdi-mailbox-open-up::before {
- content: "\FD66";
-}
-.mdi-mailbox-open-up-outline::before {
- content: "\FD67";
-}
-.mdi-mailbox-outline::before {
- content: "\FD68";
-}
-.mdi-mailbox-up::before {
- content: "\FD69";
-}
-.mdi-mailbox-up-outline::before {
- content: "\FD6A";
-}
-.mdi-map::before {
- content: "\F34D";
-}
-.mdi-map-check::before {
- content: "\FED9";
-}
-.mdi-map-check-outline::before {
- content: "\FEDA";
-}
-.mdi-map-clock::before {
- content: "\FCFA";
-}
-.mdi-map-clock-outline::before {
- content: "\FCFB";
-}
-.mdi-map-legend::before {
- content: "\FA00";
-}
-.mdi-map-marker::before {
- content: "\F34E";
-}
-.mdi-map-marker-alert::before {
- content: "\FF22";
-}
-.mdi-map-marker-alert-outline::before {
- content: "\FF23";
-}
-.mdi-map-marker-check::before {
- content: "\FC71";
-}
-.mdi-map-marker-check-outline::before {
- content: "\F0326";
-}
-.mdi-map-marker-circle::before {
- content: "\F34F";
-}
-.mdi-map-marker-distance::before {
- content: "\F8EF";
-}
-.mdi-map-marker-down::before {
- content: "\F012D";
-}
-.mdi-map-marker-left::before {
- content: "\F0306";
-}
-.mdi-map-marker-left-outline::before {
- content: "\F0308";
-}
-.mdi-map-marker-minus::before {
- content: "\F650";
-}
-.mdi-map-marker-minus-outline::before {
- content: "\F0324";
-}
-.mdi-map-marker-multiple::before {
- content: "\F350";
-}
-.mdi-map-marker-multiple-outline::before {
- content: "\F02A2";
-}
-.mdi-map-marker-off::before {
- content: "\F351";
-}
-.mdi-map-marker-off-outline::before {
- content: "\F0328";
-}
-.mdi-map-marker-outline::before {
- content: "\F7D8";
-}
-.mdi-map-marker-path::before {
- content: "\FCFC";
-}
-.mdi-map-marker-plus::before {
- content: "\F651";
-}
-.mdi-map-marker-plus-outline::before {
- content: "\F0323";
-}
-.mdi-map-marker-question::before {
- content: "\FF24";
-}
-.mdi-map-marker-question-outline::before {
- content: "\FF25";
-}
-.mdi-map-marker-radius::before {
- content: "\F352";
-}
-.mdi-map-marker-radius-outline::before {
- content: "\F0327";
-}
-.mdi-map-marker-remove::before {
- content: "\FF26";
-}
-.mdi-map-marker-remove-outline::before {
- content: "\F0325";
-}
-.mdi-map-marker-remove-variant::before {
- content: "\FF27";
-}
-.mdi-map-marker-right::before {
- content: "\F0307";
-}
-.mdi-map-marker-right-outline::before {
- content: "\F0309";
-}
-.mdi-map-marker-up::before {
- content: "\F012E";
-}
-.mdi-map-minus::before {
- content: "\F980";
-}
-.mdi-map-outline::before {
- content: "\F981";
-}
-.mdi-map-plus::before {
- content: "\F982";
-}
-.mdi-map-search::before {
- content: "\F983";
-}
-.mdi-map-search-outline::before {
- content: "\F984";
-}
-.mdi-mapbox::before {
- content: "\FB86";
-}
-.mdi-margin::before {
- content: "\F353";
-}
-.mdi-markdown::before {
- content: "\F354";
-}
-.mdi-markdown-outline::before {
- content: "\FF78";
-}
-.mdi-marker::before {
- content: "\F652";
-}
-.mdi-marker-cancel::before {
- content: "\FDB5";
-}
-.mdi-marker-check::before {
- content: "\F355";
-}
-.mdi-mastodon::before {
- content: "\FAD0";
-}
-.mdi-mastodon-variant::before {
- content: "\FAD1";
-}
-.mdi-material-design::before {
- content: "\F985";
-}
-.mdi-material-ui::before {
- content: "\F357";
-}
-.mdi-math-compass::before {
- content: "\F358";
-}
-.mdi-math-cos::before {
- content: "\FC72";
-}
-.mdi-math-integral::before {
- content: "\FFE8";
-}
-.mdi-math-integral-box::before {
- content: "\FFE9";
-}
-.mdi-math-log::before {
- content: "\F00B0";
-}
-.mdi-math-norm::before {
- content: "\FFEA";
-}
-.mdi-math-norm-box::before {
- content: "\FFEB";
-}
-.mdi-math-sin::before {
- content: "\FC73";
-}
-.mdi-math-tan::before {
- content: "\FC74";
-}
-.mdi-matrix::before {
- content: "\F628";
-}
-.mdi-medal::before {
- content: "\F986";
-}
-.mdi-medal-outline::before {
- content: "\F0351";
-}
-.mdi-medical-bag::before {
- content: "\F6EE";
-}
-.mdi-meditation::before {
- content: "\F01A6";
-}
-.mdi-medium::before {
- content: "\F35A";
-}
-.mdi-meetup::before {
- content: "\FAD2";
-}
-.mdi-memory::before {
- content: "\F35B";
-}
-.mdi-menu::before {
- content: "\F35C";
-}
-.mdi-menu-down::before {
- content: "\F35D";
-}
-.mdi-menu-down-outline::before {
- content: "\F6B5";
-}
-.mdi-menu-left::before {
- content: "\F35E";
-}
-.mdi-menu-left-outline::before {
- content: "\FA01";
-}
-.mdi-menu-open::before {
- content: "\FB87";
-}
-.mdi-menu-right::before {
- content: "\F35F";
-}
-.mdi-menu-right-outline::before {
- content: "\FA02";
-}
-.mdi-menu-swap::before {
- content: "\FA63";
-}
-.mdi-menu-swap-outline::before {
- content: "\FA64";
-}
-.mdi-menu-up::before {
- content: "\F360";
-}
-.mdi-menu-up-outline::before {
- content: "\F6B6";
-}
-.mdi-merge::before {
- content: "\FF79";
-}
-.mdi-message::before {
- content: "\F361";
-}
-.mdi-message-alert::before {
- content: "\F362";
-}
-.mdi-message-alert-outline::before {
- content: "\FA03";
-}
-.mdi-message-arrow-left::before {
- content: "\F031D";
-}
-.mdi-message-arrow-left-outline::before {
- content: "\F031E";
-}
-.mdi-message-arrow-right::before {
- content: "\F031F";
-}
-.mdi-message-arrow-right-outline::before {
- content: "\F0320";
-}
-.mdi-message-bulleted::before {
- content: "\F6A1";
-}
-.mdi-message-bulleted-off::before {
- content: "\F6A2";
-}
-.mdi-message-draw::before {
- content: "\F363";
-}
-.mdi-message-image::before {
- content: "\F364";
-}
-.mdi-message-image-outline::before {
- content: "\F0197";
-}
-.mdi-message-lock::before {
- content: "\FFEC";
-}
-.mdi-message-lock-outline::before {
- content: "\F0198";
-}
-.mdi-message-minus::before {
- content: "\F0199";
-}
-.mdi-message-minus-outline::before {
- content: "\F019A";
-}
-.mdi-message-outline::before {
- content: "\F365";
-}
-.mdi-message-plus::before {
- content: "\F653";
-}
-.mdi-message-plus-outline::before {
- content: "\F00E6";
-}
-.mdi-message-processing::before {
- content: "\F366";
-}
-.mdi-message-processing-outline::before {
- content: "\F019B";
-}
-.mdi-message-reply::before {
- content: "\F367";
-}
-.mdi-message-reply-text::before {
- content: "\F368";
-}
-.mdi-message-settings::before {
- content: "\F6EF";
-}
-.mdi-message-settings-outline::before {
- content: "\F019C";
-}
-.mdi-message-settings-variant::before {
- content: "\F6F0";
-}
-.mdi-message-settings-variant-outline::before {
- content: "\F019D";
-}
-.mdi-message-text::before {
- content: "\F369";
-}
-.mdi-message-text-clock::before {
- content: "\F019E";
-}
-.mdi-message-text-clock-outline::before {
- content: "\F019F";
-}
-.mdi-message-text-lock::before {
- content: "\FFED";
-}
-.mdi-message-text-lock-outline::before {
- content: "\F01A0";
-}
-.mdi-message-text-outline::before {
- content: "\F36A";
-}
-.mdi-message-video::before {
- content: "\F36B";
-}
-.mdi-meteor::before {
- content: "\F629";
-}
-.mdi-metronome::before {
- content: "\F7D9";
-}
-.mdi-metronome-tick::before {
- content: "\F7DA";
-}
-.mdi-micro-sd::before {
- content: "\F7DB";
-}
-.mdi-microphone::before {
- content: "\F36C";
-}
-.mdi-microphone-minus::before {
- content: "\F8B2";
-}
-.mdi-microphone-off::before {
- content: "\F36D";
-}
-.mdi-microphone-outline::before {
- content: "\F36E";
-}
-.mdi-microphone-plus::before {
- content: "\F8B3";
-}
-.mdi-microphone-settings::before {
- content: "\F36F";
-}
-.mdi-microphone-variant::before {
- content: "\F370";
-}
-.mdi-microphone-variant-off::before {
- content: "\F371";
-}
-.mdi-microscope::before {
- content: "\F654";
-}
-.mdi-microsoft::before {
- content: "\F372";
-}
-.mdi-microsoft-dynamics::before {
- content: "\F987";
-}
-.mdi-microwave::before {
- content: "\FC75";
-}
-.mdi-middleware::before {
- content: "\FF7A";
-}
-.mdi-middleware-outline::before {
- content: "\FF7B";
-}
-.mdi-midi::before {
- content: "\F8F0";
-}
-.mdi-midi-port::before {
- content: "\F8F1";
-}
-.mdi-mine::before {
- content: "\FDB6";
-}
-.mdi-minecraft::before {
- content: "\F373";
-}
-.mdi-mini-sd::before {
- content: "\FA04";
-}
-.mdi-minidisc::before {
- content: "\FA05";
-}
-.mdi-minus::before {
- content: "\F374";
-}
-.mdi-minus-box::before {
- content: "\F375";
-}
-.mdi-minus-box-multiple::before {
- content: "\F016C";
-}
-.mdi-minus-box-multiple-outline::before {
- content: "\F016D";
-}
-.mdi-minus-box-outline::before {
- content: "\F6F1";
-}
-.mdi-minus-circle::before {
- content: "\F376";
-}
-.mdi-minus-circle-outline::before {
- content: "\F377";
-}
-.mdi-minus-network::before {
- content: "\F378";
-}
-.mdi-minus-network-outline::before {
- content: "\FC76";
-}
-.mdi-mirror::before {
- content: "\F0228";
-}
-.mdi-mixcloud::before {
- content: "\F62A";
-}
-.mdi-mixed-martial-arts::before {
- content: "\FD6B";
-}
-.mdi-mixed-reality::before {
- content: "\F87E";
-}
-.mdi-mixer::before {
- content: "\F7DC";
-}
-.mdi-molecule::before {
- content: "\FB88";
-}
-.mdi-monitor::before {
- content: "\F379";
-}
-.mdi-monitor-cellphone::before {
- content: "\F988";
-}
-.mdi-monitor-cellphone-star::before {
- content: "\F989";
-}
-.mdi-monitor-clean::before {
- content: "\F012F";
-}
-.mdi-monitor-dashboard::before {
- content: "\FA06";
-}
-.mdi-monitor-edit::before {
- content: "\F02F1";
-}
-.mdi-monitor-lock::before {
- content: "\FDB7";
-}
-.mdi-monitor-multiple::before {
- content: "\F37A";
-}
-.mdi-monitor-off::before {
- content: "\FD6C";
-}
-.mdi-monitor-screenshot::before {
- content: "\FE34";
-}
-.mdi-monitor-speaker::before {
- content: "\FF7C";
-}
-.mdi-monitor-speaker-off::before {
- content: "\FF7D";
-}
-.mdi-monitor-star::before {
- content: "\FDB8";
-}
-.mdi-moon-first-quarter::before {
- content: "\FF7E";
-}
-.mdi-moon-full::before {
- content: "\FF7F";
-}
-.mdi-moon-last-quarter::before {
- content: "\FF80";
-}
-.mdi-moon-new::before {
- content: "\FF81";
-}
-.mdi-moon-waning-crescent::before {
- content: "\FF82";
-}
-.mdi-moon-waning-gibbous::before {
- content: "\FF83";
-}
-.mdi-moon-waxing-crescent::before {
- content: "\FF84";
-}
-.mdi-moon-waxing-gibbous::before {
- content: "\FF85";
-}
-.mdi-moped::before {
- content: "\F00B1";
-}
-.mdi-more::before {
- content: "\F37B";
-}
-.mdi-mother-heart::before {
- content: "\F033F";
-}
-.mdi-mother-nurse::before {
- content: "\FCFD";
-}
-.mdi-motion-sensor::before {
- content: "\FD6D";
-}
-.mdi-motorbike::before {
- content: "\F37C";
-}
-.mdi-mouse::before {
- content: "\F37D";
-}
-.mdi-mouse-bluetooth::before {
- content: "\F98A";
-}
-.mdi-mouse-off::before {
- content: "\F37E";
-}
-.mdi-mouse-variant::before {
- content: "\F37F";
-}
-.mdi-mouse-variant-off::before {
- content: "\F380";
-}
-.mdi-move-resize::before {
- content: "\F655";
-}
-.mdi-move-resize-variant::before {
- content: "\F656";
-}
-.mdi-movie::before {
- content: "\F381";
-}
-.mdi-movie-edit::before {
- content: "\F014D";
-}
-.mdi-movie-edit-outline::before {
- content: "\F014E";
-}
-.mdi-movie-filter::before {
- content: "\F014F";
-}
-.mdi-movie-filter-outline::before {
- content: "\F0150";
-}
-.mdi-movie-open::before {
- content: "\FFEE";
-}
-.mdi-movie-open-outline::before {
- content: "\FFEF";
-}
-.mdi-movie-outline::before {
- content: "\FDB9";
-}
-.mdi-movie-roll::before {
- content: "\F7DD";
-}
-.mdi-movie-search::before {
- content: "\F01FD";
-}
-.mdi-movie-search-outline::before {
- content: "\F01FE";
-}
-.mdi-muffin::before {
- content: "\F98B";
-}
-.mdi-multiplication::before {
- content: "\F382";
-}
-.mdi-multiplication-box::before {
- content: "\F383";
-}
-.mdi-mushroom::before {
- content: "\F7DE";
-}
-.mdi-mushroom-outline::before {
- content: "\F7DF";
-}
-.mdi-music::before {
- content: "\F759";
-}
-.mdi-music-accidental-double-flat::before {
- content: "\FF86";
-}
-.mdi-music-accidental-double-sharp::before {
- content: "\FF87";
-}
-.mdi-music-accidental-flat::before {
- content: "\FF88";
-}
-.mdi-music-accidental-natural::before {
- content: "\FF89";
-}
-.mdi-music-accidental-sharp::before {
- content: "\FF8A";
-}
-.mdi-music-box::before {
- content: "\F384";
-}
-.mdi-music-box-outline::before {
- content: "\F385";
-}
-.mdi-music-circle::before {
- content: "\F386";
-}
-.mdi-music-circle-outline::before {
- content: "\FAD3";
-}
-.mdi-music-clef-alto::before {
- content: "\FF8B";
-}
-.mdi-music-clef-bass::before {
- content: "\FF8C";
-}
-.mdi-music-clef-treble::before {
- content: "\FF8D";
-}
-.mdi-music-note::before {
- content: "\F387";
-}
-.mdi-music-note-bluetooth::before {
- content: "\F5FE";
-}
-.mdi-music-note-bluetooth-off::before {
- content: "\F5FF";
-}
-.mdi-music-note-eighth::before {
- content: "\F388";
-}
-.mdi-music-note-eighth-dotted::before {
- content: "\FF8E";
-}
-.mdi-music-note-half::before {
- content: "\F389";
-}
-.mdi-music-note-half-dotted::before {
- content: "\FF8F";
-}
-.mdi-music-note-off::before {
- content: "\F38A";
-}
-.mdi-music-note-off-outline::before {
- content: "\FF90";
-}
-.mdi-music-note-outline::before {
- content: "\FF91";
-}
-.mdi-music-note-plus::before {
- content: "\FDBA";
-}
-.mdi-music-note-quarter::before {
- content: "\F38B";
-}
-.mdi-music-note-quarter-dotted::before {
- content: "\FF92";
-}
-.mdi-music-note-sixteenth::before {
- content: "\F38C";
-}
-.mdi-music-note-sixteenth-dotted::before {
- content: "\FF93";
-}
-.mdi-music-note-whole::before {
- content: "\F38D";
-}
-.mdi-music-note-whole-dotted::before {
- content: "\FF94";
-}
-.mdi-music-off::before {
- content: "\F75A";
-}
-.mdi-music-rest-eighth::before {
- content: "\FF95";
-}
-.mdi-music-rest-half::before {
- content: "\FF96";
-}
-.mdi-music-rest-quarter::before {
- content: "\FF97";
-}
-.mdi-music-rest-sixteenth::before {
- content: "\FF98";
-}
-.mdi-music-rest-whole::before {
- content: "\FF99";
-}
-.mdi-nail::before {
- content: "\FDBB";
-}
-.mdi-nas::before {
- content: "\F8F2";
-}
-.mdi-nativescript::before {
- content: "\F87F";
-}
-.mdi-nature::before {
- content: "\F38E";
-}
-.mdi-nature-people::before {
- content: "\F38F";
-}
-.mdi-navigation::before {
- content: "\F390";
-}
-.mdi-near-me::before {
- content: "\F5CD";
-}
-.mdi-necklace::before {
- content: "\FF28";
-}
-.mdi-needle::before {
- content: "\F391";
-}
-.mdi-netflix::before {
- content: "\F745";
-}
-.mdi-network::before {
- content: "\F6F2";
-}
-.mdi-network-off::before {
- content: "\FC77";
-}
-.mdi-network-off-outline::before {
- content: "\FC78";
-}
-.mdi-network-outline::before {
- content: "\FC79";
-}
-.mdi-network-router::before {
- content: "\F00B2";
-}
-.mdi-network-strength-1::before {
- content: "\F8F3";
-}
-.mdi-network-strength-1-alert::before {
- content: "\F8F4";
-}
-.mdi-network-strength-2::before {
- content: "\F8F5";
-}
-.mdi-network-strength-2-alert::before {
- content: "\F8F6";
-}
-.mdi-network-strength-3::before {
- content: "\F8F7";
-}
-.mdi-network-strength-3-alert::before {
- content: "\F8F8";
-}
-.mdi-network-strength-4::before {
- content: "\F8F9";
-}
-.mdi-network-strength-4-alert::before {
- content: "\F8FA";
-}
-.mdi-network-strength-off::before {
- content: "\F8FB";
-}
-.mdi-network-strength-off-outline::before {
- content: "\F8FC";
-}
-.mdi-network-strength-outline::before {
- content: "\F8FD";
-}
-.mdi-new-box::before {
- content: "\F394";
-}
-.mdi-newspaper::before {
- content: "\F395";
-}
-.mdi-newspaper-minus::before {
- content: "\FF29";
-}
-.mdi-newspaper-plus::before {
- content: "\FF2A";
-}
-.mdi-newspaper-variant::before {
- content: "\F0023";
-}
-.mdi-newspaper-variant-multiple::before {
- content: "\F0024";
-}
-.mdi-newspaper-variant-multiple-outline::before {
- content: "\F0025";
-}
-.mdi-newspaper-variant-outline::before {
- content: "\F0026";
-}
-.mdi-nfc::before {
- content: "\F396";
-}
-.mdi-nfc-off::before {
- content: "\FE35";
-}
-.mdi-nfc-search-variant::before {
- content: "\FE36";
-}
-.mdi-nfc-tap::before {
- content: "\F397";
-}
-.mdi-nfc-variant::before {
- content: "\F398";
-}
-.mdi-nfc-variant-off::before {
- content: "\FE37";
-}
-.mdi-ninja::before {
- content: "\F773";
-}
-.mdi-nintendo-switch::before {
- content: "\F7E0";
-}
-.mdi-nix::before {
- content: "\F0130";
-}
-.mdi-nodejs::before {
- content: "\F399";
-}
-.mdi-noodles::before {
- content: "\F01A9";
-}
-.mdi-not-equal::before {
- content: "\F98C";
-}
-.mdi-not-equal-variant::before {
- content: "\F98D";
-}
-.mdi-note::before {
- content: "\F39A";
-}
-.mdi-note-multiple::before {
- content: "\F6B7";
-}
-.mdi-note-multiple-outline::before {
- content: "\F6B8";
-}
-.mdi-note-outline::before {
- content: "\F39B";
-}
-.mdi-note-plus::before {
- content: "\F39C";
-}
-.mdi-note-plus-outline::before {
- content: "\F39D";
-}
-.mdi-note-text::before {
- content: "\F39E";
-}
-.mdi-note-text-outline::before {
- content: "\F0202";
-}
-.mdi-notebook::before {
- content: "\F82D";
-}
-.mdi-notebook-multiple::before {
- content: "\FE38";
-}
-.mdi-notebook-outline::before {
- content: "\FEDC";
-}
-.mdi-notification-clear-all::before {
- content: "\F39F";
-}
-.mdi-npm::before {
- content: "\F6F6";
-}
-.mdi-npm-variant::before {
- content: "\F98E";
-}
-.mdi-npm-variant-outline::before {
- content: "\F98F";
-}
-.mdi-nuke::before {
- content: "\F6A3";
-}
-.mdi-null::before {
- content: "\F7E1";
-}
-.mdi-numeric::before {
- content: "\F3A0";
-}
-.mdi-numeric-0::before {
- content: "\30";
-}
-.mdi-numeric-0-box::before {
- content: "\F3A1";
-}
-.mdi-numeric-0-box-multiple::before {
- content: "\FF2B";
-}
-.mdi-numeric-0-box-multiple-outline::before {
- content: "\F3A2";
-}
-.mdi-numeric-0-box-outline::before {
- content: "\F3A3";
-}
-.mdi-numeric-0-circle::before {
- content: "\FC7A";
-}
-.mdi-numeric-0-circle-outline::before {
- content: "\FC7B";
-}
-.mdi-numeric-1::before {
- content: "\31";
-}
-.mdi-numeric-1-box::before {
- content: "\F3A4";
-}
-.mdi-numeric-1-box-multiple::before {
- content: "\FF2C";
-}
-.mdi-numeric-1-box-multiple-outline::before {
- content: "\F3A5";
-}
-.mdi-numeric-1-box-outline::before {
- content: "\F3A6";
-}
-.mdi-numeric-1-circle::before {
- content: "\FC7C";
-}
-.mdi-numeric-1-circle-outline::before {
- content: "\FC7D";
-}
-.mdi-numeric-10::before {
- content: "\F000A";
-}
-.mdi-numeric-10-box::before {
- content: "\FF9A";
-}
-.mdi-numeric-10-box-multiple::before {
- content: "\F000B";
-}
-.mdi-numeric-10-box-multiple-outline::before {
- content: "\F000C";
-}
-.mdi-numeric-10-box-outline::before {
- content: "\FF9B";
-}
-.mdi-numeric-10-circle::before {
- content: "\F000D";
-}
-.mdi-numeric-10-circle-outline::before {
- content: "\F000E";
-}
-.mdi-numeric-2::before {
- content: "\32";
-}
-.mdi-numeric-2-box::before {
- content: "\F3A7";
-}
-.mdi-numeric-2-box-multiple::before {
- content: "\FF2D";
-}
-.mdi-numeric-2-box-multiple-outline::before {
- content: "\F3A8";
-}
-.mdi-numeric-2-box-outline::before {
- content: "\F3A9";
-}
-.mdi-numeric-2-circle::before {
- content: "\FC7E";
-}
-.mdi-numeric-2-circle-outline::before {
- content: "\FC7F";
-}
-.mdi-numeric-3::before {
- content: "\33";
-}
-.mdi-numeric-3-box::before {
- content: "\F3AA";
-}
-.mdi-numeric-3-box-multiple::before {
- content: "\FF2E";
-}
-.mdi-numeric-3-box-multiple-outline::before {
- content: "\F3AB";
-}
-.mdi-numeric-3-box-outline::before {
- content: "\F3AC";
-}
-.mdi-numeric-3-circle::before {
- content: "\FC80";
-}
-.mdi-numeric-3-circle-outline::before {
- content: "\FC81";
-}
-.mdi-numeric-4::before {
- content: "\34";
-}
-.mdi-numeric-4-box::before {
- content: "\F3AD";
-}
-.mdi-numeric-4-box-multiple::before {
- content: "\FF2F";
-}
-.mdi-numeric-4-box-multiple-outline::before {
- content: "\F3AE";
-}
-.mdi-numeric-4-box-outline::before {
- content: "\F3AF";
-}
-.mdi-numeric-4-circle::before {
- content: "\FC82";
-}
-.mdi-numeric-4-circle-outline::before {
- content: "\FC83";
-}
-.mdi-numeric-5::before {
- content: "\35";
-}
-.mdi-numeric-5-box::before {
- content: "\F3B0";
-}
-.mdi-numeric-5-box-multiple::before {
- content: "\FF30";
-}
-.mdi-numeric-5-box-multiple-outline::before {
- content: "\F3B1";
-}
-.mdi-numeric-5-box-outline::before {
- content: "\F3B2";
-}
-.mdi-numeric-5-circle::before {
- content: "\FC84";
-}
-.mdi-numeric-5-circle-outline::before {
- content: "\FC85";
-}
-.mdi-numeric-6::before {
- content: "\36";
-}
-.mdi-numeric-6-box::before {
- content: "\F3B3";
-}
-.mdi-numeric-6-box-multiple::before {
- content: "\FF31";
-}
-.mdi-numeric-6-box-multiple-outline::before {
- content: "\F3B4";
-}
-.mdi-numeric-6-box-outline::before {
- content: "\F3B5";
-}
-.mdi-numeric-6-circle::before {
- content: "\FC86";
-}
-.mdi-numeric-6-circle-outline::before {
- content: "\FC87";
-}
-.mdi-numeric-7::before {
- content: "\37";
-}
-.mdi-numeric-7-box::before {
- content: "\F3B6";
-}
-.mdi-numeric-7-box-multiple::before {
- content: "\FF32";
-}
-.mdi-numeric-7-box-multiple-outline::before {
- content: "\F3B7";
-}
-.mdi-numeric-7-box-outline::before {
- content: "\F3B8";
-}
-.mdi-numeric-7-circle::before {
- content: "\FC88";
-}
-.mdi-numeric-7-circle-outline::before {
- content: "\FC89";
-}
-.mdi-numeric-8::before {
- content: "\38";
-}
-.mdi-numeric-8-box::before {
- content: "\F3B9";
-}
-.mdi-numeric-8-box-multiple::before {
- content: "\FF33";
-}
-.mdi-numeric-8-box-multiple-outline::before {
- content: "\F3BA";
-}
-.mdi-numeric-8-box-outline::before {
- content: "\F3BB";
-}
-.mdi-numeric-8-circle::before {
- content: "\FC8A";
-}
-.mdi-numeric-8-circle-outline::before {
- content: "\FC8B";
-}
-.mdi-numeric-9::before {
- content: "\39";
-}
-.mdi-numeric-9-box::before {
- content: "\F3BC";
-}
-.mdi-numeric-9-box-multiple::before {
- content: "\FF34";
-}
-.mdi-numeric-9-box-multiple-outline::before {
- content: "\F3BD";
-}
-.mdi-numeric-9-box-outline::before {
- content: "\F3BE";
-}
-.mdi-numeric-9-circle::before {
- content: "\FC8C";
-}
-.mdi-numeric-9-circle-outline::before {
- content: "\FC8D";
-}
-.mdi-numeric-9-plus::before {
- content: "\F000F";
-}
-.mdi-numeric-9-plus-box::before {
- content: "\F3BF";
-}
-.mdi-numeric-9-plus-box-multiple::before {
- content: "\FF35";
-}
-.mdi-numeric-9-plus-box-multiple-outline::before {
- content: "\F3C0";
-}
-.mdi-numeric-9-plus-box-outline::before {
- content: "\F3C1";
-}
-.mdi-numeric-9-plus-circle::before {
- content: "\FC8E";
-}
-.mdi-numeric-9-plus-circle-outline::before {
- content: "\FC8F";
-}
-.mdi-numeric-negative-1::before {
- content: "\F0074";
-}
-.mdi-nut::before {
- content: "\F6F7";
-}
-.mdi-nutrition::before {
- content: "\F3C2";
-}
-.mdi-nuxt::before {
- content: "\F0131";
-}
-.mdi-oar::before {
- content: "\F67B";
-}
-.mdi-ocarina::before {
- content: "\FDBC";
-}
-.mdi-oci::before {
- content: "\F0314";
-}
-.mdi-ocr::before {
- content: "\F0165";
-}
-.mdi-octagon::before {
- content: "\F3C3";
-}
-.mdi-octagon-outline::before {
- content: "\F3C4";
-}
-.mdi-octagram::before {
- content: "\F6F8";
-}
-.mdi-octagram-outline::before {
- content: "\F774";
-}
-.mdi-odnoklassniki::before {
- content: "\F3C5";
-}
-.mdi-offer::before {
- content: "\F0246";
-}
-.mdi-office::before {
- content: "\F3C6";
-}
-.mdi-office-building::before {
- content: "\F990";
-}
-.mdi-oil::before {
- content: "\F3C7";
-}
-.mdi-oil-lamp::before {
- content: "\FF36";
-}
-.mdi-oil-level::before {
- content: "\F0075";
-}
-.mdi-oil-temperature::before {
- content: "\F0019";
-}
-.mdi-omega::before {
- content: "\F3C9";
-}
-.mdi-one-up::before {
- content: "\FB89";
-}
-.mdi-onedrive::before {
- content: "\F3CA";
-}
-.mdi-onenote::before {
- content: "\F746";
-}
-.mdi-onepassword::before {
- content: "\F880";
-}
-.mdi-opacity::before {
- content: "\F5CC";
-}
-.mdi-open-in-app::before {
- content: "\F3CB";
-}
-.mdi-open-in-new::before {
- content: "\F3CC";
-}
-.mdi-open-source-initiative::before {
- content: "\FB8A";
-}
-.mdi-openid::before {
- content: "\F3CD";
-}
-.mdi-opera::before {
- content: "\F3CE";
-}
-.mdi-orbit::before {
- content: "\F018";
-}
-.mdi-origin::before {
- content: "\FB2B";
-}
-.mdi-ornament::before {
- content: "\F3CF";
-}
-.mdi-ornament-variant::before {
- content: "\F3D0";
-}
-.mdi-outdoor-lamp::before {
- content: "\F0076";
-}
-.mdi-outlook::before {
- content: "\FCFE";
-}
-.mdi-overscan::before {
- content: "\F0027";
-}
-.mdi-owl::before {
- content: "\F3D2";
-}
-.mdi-pac-man::before {
- content: "\FB8B";
-}
-.mdi-package::before {
- content: "\F3D3";
-}
-.mdi-package-down::before {
- content: "\F3D4";
-}
-.mdi-package-up::before {
- content: "\F3D5";
-}
-.mdi-package-variant::before {
- content: "\F3D6";
-}
-.mdi-package-variant-closed::before {
- content: "\F3D7";
-}
-.mdi-page-first::before {
- content: "\F600";
-}
-.mdi-page-last::before {
- content: "\F601";
-}
-.mdi-page-layout-body::before {
- content: "\F6F9";
-}
-.mdi-page-layout-footer::before {
- content: "\F6FA";
-}
-.mdi-page-layout-header::before {
- content: "\F6FB";
-}
-.mdi-page-layout-header-footer::before {
- content: "\FF9C";
-}
-.mdi-page-layout-sidebar-left::before {
- content: "\F6FC";
-}
-.mdi-page-layout-sidebar-right::before {
- content: "\F6FD";
-}
-.mdi-page-next::before {
- content: "\FB8C";
-}
-.mdi-page-next-outline::before {
- content: "\FB8D";
-}
-.mdi-page-previous::before {
- content: "\FB8E";
-}
-.mdi-page-previous-outline::before {
- content: "\FB8F";
-}
-.mdi-palette::before {
- content: "\F3D8";
-}
-.mdi-palette-advanced::before {
- content: "\F3D9";
-}
-.mdi-palette-outline::before {
- content: "\FE6C";
-}
-.mdi-palette-swatch::before {
- content: "\F8B4";
-}
-.mdi-palette-swatch-outline::before {
- content: "\F0387";
-}
-.mdi-palm-tree::before {
- content: "\F0077";
-}
-.mdi-pan::before {
- content: "\FB90";
-}
-.mdi-pan-bottom-left::before {
- content: "\FB91";
-}
-.mdi-pan-bottom-right::before {
- content: "\FB92";
-}
-.mdi-pan-down::before {
- content: "\FB93";
-}
-.mdi-pan-horizontal::before {
- content: "\FB94";
-}
-.mdi-pan-left::before {
- content: "\FB95";
-}
-.mdi-pan-right::before {
- content: "\FB96";
-}
-.mdi-pan-top-left::before {
- content: "\FB97";
-}
-.mdi-pan-top-right::before {
- content: "\FB98";
-}
-.mdi-pan-up::before {
- content: "\FB99";
-}
-.mdi-pan-vertical::before {
- content: "\FB9A";
-}
-.mdi-panda::before {
- content: "\F3DA";
-}
-.mdi-pandora::before {
- content: "\F3DB";
-}
-.mdi-panorama::before {
- content: "\F3DC";
-}
-.mdi-panorama-fisheye::before {
- content: "\F3DD";
-}
-.mdi-panorama-horizontal::before {
- content: "\F3DE";
-}
-.mdi-panorama-vertical::before {
- content: "\F3DF";
-}
-.mdi-panorama-wide-angle::before {
- content: "\F3E0";
-}
-.mdi-paper-cut-vertical::before {
- content: "\F3E1";
-}
-.mdi-paper-roll::before {
- content: "\F0182";
-}
-.mdi-paper-roll-outline::before {
- content: "\F0183";
-}
-.mdi-paperclip::before {
- content: "\F3E2";
-}
-.mdi-parachute::before {
- content: "\FC90";
-}
-.mdi-parachute-outline::before {
- content: "\FC91";
-}
-.mdi-parking::before {
- content: "\F3E3";
-}
-.mdi-party-popper::before {
- content: "\F0078";
-}
-.mdi-passport::before {
- content: "\F7E2";
-}
-.mdi-passport-biometric::before {
- content: "\FDBD";
-}
-.mdi-pasta::before {
- content: "\F018B";
-}
-.mdi-patio-heater::before {
- content: "\FF9D";
-}
-.mdi-patreon::before {
- content: "\F881";
-}
-.mdi-pause::before {
- content: "\F3E4";
-}
-.mdi-pause-circle::before {
- content: "\F3E5";
-}
-.mdi-pause-circle-outline::before {
- content: "\F3E6";
-}
-.mdi-pause-octagon::before {
- content: "\F3E7";
-}
-.mdi-pause-octagon-outline::before {
- content: "\F3E8";
-}
-.mdi-paw::before {
- content: "\F3E9";
-}
-.mdi-paw-off::before {
- content: "\F657";
-}
-.mdi-paypal::before {
- content: "\F882";
-}
-.mdi-pdf-box::before {
- content: "\FE39";
-}
-.mdi-peace::before {
- content: "\F883";
-}
-.mdi-peanut::before {
- content: "\F001E";
-}
-.mdi-peanut-off::before {
- content: "\F001F";
-}
-.mdi-peanut-off-outline::before {
- content: "\F0021";
-}
-.mdi-peanut-outline::before {
- content: "\F0020";
-}
-.mdi-pen::before {
- content: "\F3EA";
-}
-.mdi-pen-lock::before {
- content: "\FDBE";
-}
-.mdi-pen-minus::before {
- content: "\FDBF";
-}
-.mdi-pen-off::before {
- content: "\FDC0";
-}
-.mdi-pen-plus::before {
- content: "\FDC1";
-}
-.mdi-pen-remove::before {
- content: "\FDC2";
-}
-.mdi-pencil::before {
- content: "\F3EB";
-}
-.mdi-pencil-box::before {
- content: "\F3EC";
-}
-.mdi-pencil-box-multiple::before {
- content: "\F016F";
-}
-.mdi-pencil-box-multiple-outline::before {
- content: "\F0170";
-}
-.mdi-pencil-box-outline::before {
- content: "\F3ED";
-}
-.mdi-pencil-circle::before {
- content: "\F6FE";
-}
-.mdi-pencil-circle-outline::before {
- content: "\F775";
-}
-.mdi-pencil-lock::before {
- content: "\F3EE";
-}
-.mdi-pencil-lock-outline::before {
- content: "\FDC3";
-}
-.mdi-pencil-minus::before {
- content: "\FDC4";
-}
-.mdi-pencil-minus-outline::before {
- content: "\FDC5";
-}
-.mdi-pencil-off::before {
- content: "\F3EF";
-}
-.mdi-pencil-off-outline::before {
- content: "\FDC6";
-}
-.mdi-pencil-outline::before {
- content: "\FC92";
-}
-.mdi-pencil-plus::before {
- content: "\FDC7";
-}
-.mdi-pencil-plus-outline::before {
- content: "\FDC8";
-}
-.mdi-pencil-remove::before {
- content: "\FDC9";
-}
-.mdi-pencil-remove-outline::before {
- content: "\FDCA";
-}
-.mdi-pencil-ruler::before {
- content: "\F037E";
-}
-.mdi-penguin::before {
- content: "\FEDD";
-}
-.mdi-pentagon::before {
- content: "\F6FF";
-}
-.mdi-pentagon-outline::before {
- content: "\F700";
-}
-.mdi-percent::before {
- content: "\F3F0";
-}
-.mdi-percent-outline::before {
- content: "\F02A3";
-}
-.mdi-periodic-table::before {
- content: "\F8B5";
-}
-.mdi-periodic-table-co::before {
- content: "\F0329";
-}
-.mdi-periodic-table-co2::before {
- content: "\F7E3";
-}
-.mdi-periscope::before {
- content: "\F747";
-}
-.mdi-perspective-less::before {
- content: "\FCFF";
-}
-.mdi-perspective-more::before {
- content: "\FD00";
-}
-.mdi-pharmacy::before {
- content: "\F3F1";
-}
-.mdi-phone::before {
- content: "\F3F2";
-}
-.mdi-phone-alert::before {
- content: "\FF37";
-}
-.mdi-phone-alert-outline::before {
- content: "\F01B9";
-}
-.mdi-phone-bluetooth::before {
- content: "\F3F3";
-}
-.mdi-phone-bluetooth-outline::before {
- content: "\F01BA";
-}
-.mdi-phone-cancel::before {
- content: "\F00E7";
-}
-.mdi-phone-cancel-outline::before {
- content: "\F01BB";
-}
-.mdi-phone-check::before {
- content: "\F01D4";
-}
-.mdi-phone-check-outline::before {
- content: "\F01D5";
-}
-.mdi-phone-classic::before {
- content: "\F602";
-}
-.mdi-phone-classic-off::before {
- content: "\F02A4";
-}
-.mdi-phone-forward::before {
- content: "\F3F4";
-}
-.mdi-phone-forward-outline::before {
- content: "\F01BC";
-}
-.mdi-phone-hangup::before {
- content: "\F3F5";
-}
-.mdi-phone-hangup-outline::before {
- content: "\F01BD";
-}
-.mdi-phone-in-talk::before {
- content: "\F3F6";
-}
-.mdi-phone-in-talk-outline::before {
- content: "\F01AD";
-}
-.mdi-phone-incoming::before {
- content: "\F3F7";
-}
-.mdi-phone-incoming-outline::before {
- content: "\F01BE";
-}
-.mdi-phone-lock::before {
- content: "\F3F8";
-}
-.mdi-phone-lock-outline::before {
- content: "\F01BF";
-}
-.mdi-phone-log::before {
- content: "\F3F9";
-}
-.mdi-phone-log-outline::before {
- content: "\F01C0";
-}
-.mdi-phone-message::before {
- content: "\F01C1";
-}
-.mdi-phone-message-outline::before {
- content: "\F01C2";
-}
-.mdi-phone-minus::before {
- content: "\F658";
-}
-.mdi-phone-minus-outline::before {
- content: "\F01C3";
-}
-.mdi-phone-missed::before {
- content: "\F3FA";
-}
-.mdi-phone-missed-outline::before {
- content: "\F01D0";
-}
-.mdi-phone-off::before {
- content: "\FDCB";
-}
-.mdi-phone-off-outline::before {
- content: "\F01D1";
-}
-.mdi-phone-outgoing::before {
- content: "\F3FB";
-}
-.mdi-phone-outgoing-outline::before {
- content: "\F01C4";
-}
-.mdi-phone-outline::before {
- content: "\FDCC";
-}
-.mdi-phone-paused::before {
- content: "\F3FC";
-}
-.mdi-phone-paused-outline::before {
- content: "\F01C5";
-}
-.mdi-phone-plus::before {
- content: "\F659";
-}
-.mdi-phone-plus-outline::before {
- content: "\F01C6";
-}
-.mdi-phone-return::before {
- content: "\F82E";
-}
-.mdi-phone-return-outline::before {
- content: "\F01C7";
-}
-.mdi-phone-ring::before {
- content: "\F01D6";
-}
-.mdi-phone-ring-outline::before {
- content: "\F01D7";
-}
-.mdi-phone-rotate-landscape::before {
- content: "\F884";
-}
-.mdi-phone-rotate-portrait::before {
- content: "\F885";
-}
-.mdi-phone-settings::before {
- content: "\F3FD";
-}
-.mdi-phone-settings-outline::before {
- content: "\F01C8";
-}
-.mdi-phone-voip::before {
- content: "\F3FE";
-}
-.mdi-pi::before {
- content: "\F3FF";
-}
-.mdi-pi-box::before {
- content: "\F400";
-}
-.mdi-pi-hole::before {
- content: "\FDCD";
-}
-.mdi-piano::before {
- content: "\F67C";
-}
-.mdi-pickaxe::before {
- content: "\F8B6";
-}
-.mdi-picture-in-picture-bottom-right::before {
- content: "\FE3A";
-}
-.mdi-picture-in-picture-bottom-right-outline::before {
- content: "\FE3B";
-}
-.mdi-picture-in-picture-top-right::before {
- content: "\FE3C";
-}
-.mdi-picture-in-picture-top-right-outline::before {
- content: "\FE3D";
-}
-.mdi-pier::before {
- content: "\F886";
-}
-.mdi-pier-crane::before {
- content: "\F887";
-}
-.mdi-pig::before {
- content: "\F401";
-}
-.mdi-pig-variant::before {
- content: "\F0028";
-}
-.mdi-piggy-bank::before {
- content: "\F0029";
-}
-.mdi-pill::before {
- content: "\F402";
-}
-.mdi-pillar::before {
- content: "\F701";
-}
-.mdi-pin::before {
- content: "\F403";
-}
-.mdi-pin-off::before {
- content: "\F404";
-}
-.mdi-pin-off-outline::before {
- content: "\F92F";
-}
-.mdi-pin-outline::before {
- content: "\F930";
-}
-.mdi-pine-tree::before {
- content: "\F405";
-}
-.mdi-pine-tree-box::before {
- content: "\F406";
-}
-.mdi-pinterest::before {
- content: "\F407";
-}
-.mdi-pinterest-box::before {
- content: "\F408";
-}
-.mdi-pinwheel::before {
- content: "\FAD4";
-}
-.mdi-pinwheel-outline::before {
- content: "\FAD5";
-}
-.mdi-pipe::before {
- content: "\F7E4";
-}
-.mdi-pipe-disconnected::before {
- content: "\F7E5";
-}
-.mdi-pipe-leak::before {
- content: "\F888";
-}
-.mdi-pipe-wrench::before {
- content: "\F037F";
-}
-.mdi-pirate::before {
- content: "\FA07";
-}
-.mdi-pistol::before {
- content: "\F702";
-}
-.mdi-piston::before {
- content: "\F889";
-}
-.mdi-pizza::before {
- content: "\F409";
-}
-.mdi-play::before {
- content: "\F40A";
-}
-.mdi-play-box::before {
- content: "\F02A5";
-}
-.mdi-play-box-outline::before {
- content: "\F40B";
-}
-.mdi-play-circle::before {
- content: "\F40C";
-}
-.mdi-play-circle-outline::before {
- content: "\F40D";
-}
-.mdi-play-network::before {
- content: "\F88A";
-}
-.mdi-play-network-outline::before {
- content: "\FC93";
-}
-.mdi-play-outline::before {
- content: "\FF38";
-}
-.mdi-play-pause::before {
- content: "\F40E";
-}
-.mdi-play-protected-content::before {
- content: "\F40F";
-}
-.mdi-play-speed::before {
- content: "\F8FE";
-}
-.mdi-playlist-check::before {
- content: "\F5C7";
-}
-.mdi-playlist-edit::before {
- content: "\F8FF";
-}
-.mdi-playlist-minus::before {
- content: "\F410";
-}
-.mdi-playlist-music::before {
- content: "\FC94";
-}
-.mdi-playlist-music-outline::before {
- content: "\FC95";
-}
-.mdi-playlist-play::before {
- content: "\F411";
-}
-.mdi-playlist-plus::before {
- content: "\F412";
-}
-.mdi-playlist-remove::before {
- content: "\F413";
-}
-.mdi-playlist-star::before {
- content: "\FDCE";
-}
-.mdi-playstation::before {
- content: "\F414";
-}
-.mdi-plex::before {
- content: "\F6B9";
-}
-.mdi-plus::before {
- content: "\F415";
-}
-.mdi-plus-box::before {
- content: "\F416";
-}
-.mdi-plus-box-multiple::before {
- content: "\F334";
-}
-.mdi-plus-box-multiple-outline::before {
- content: "\F016E";
-}
-.mdi-plus-box-outline::before {
- content: "\F703";
-}
-.mdi-plus-circle::before {
- content: "\F417";
-}
-.mdi-plus-circle-multiple-outline::before {
- content: "\F418";
-}
-.mdi-plus-circle-outline::before {
- content: "\F419";
-}
-.mdi-plus-minus::before {
- content: "\F991";
-}
-.mdi-plus-minus-box::before {
- content: "\F992";
-}
-.mdi-plus-network::before {
- content: "\F41A";
-}
-.mdi-plus-network-outline::before {
- content: "\FC96";
-}
-.mdi-plus-one::before {
- content: "\F41B";
-}
-.mdi-plus-outline::before {
- content: "\F704";
-}
-.mdi-plus-thick::before {
- content: "\F0217";
-}
-.mdi-pocket::before {
- content: "\F41C";
-}
-.mdi-podcast::before {
- content: "\F993";
-}
-.mdi-podium::before {
- content: "\FD01";
-}
-.mdi-podium-bronze::before {
- content: "\FD02";
-}
-.mdi-podium-gold::before {
- content: "\FD03";
-}
-.mdi-podium-silver::before {
- content: "\FD04";
-}
-.mdi-point-of-sale::before {
- content: "\FD6E";
-}
-.mdi-pokeball::before {
- content: "\F41D";
-}
-.mdi-pokemon-go::before {
- content: "\FA08";
-}
-.mdi-poker-chip::before {
- content: "\F82F";
-}
-.mdi-polaroid::before {
- content: "\F41E";
-}
-.mdi-police-badge::before {
- content: "\F0192";
-}
-.mdi-police-badge-outline::before {
- content: "\F0193";
-}
-.mdi-poll::before {
- content: "\F41F";
-}
-.mdi-poll-box::before {
- content: "\F420";
-}
-.mdi-poll-box-outline::before {
- content: "\F02A6";
-}
-.mdi-polymer::before {
- content: "\F421";
-}
-.mdi-pool::before {
- content: "\F606";
-}
-.mdi-popcorn::before {
- content: "\F422";
-}
-.mdi-post::before {
- content: "\F002A";
-}
-.mdi-post-outline::before {
- content: "\F002B";
-}
-.mdi-postage-stamp::before {
- content: "\FC97";
-}
-.mdi-pot::before {
- content: "\F65A";
-}
-.mdi-pot-mix::before {
- content: "\F65B";
-}
-.mdi-pound::before {
- content: "\F423";
-}
-.mdi-pound-box::before {
- content: "\F424";
-}
-.mdi-pound-box-outline::before {
- content: "\F01AA";
-}
-.mdi-power::before {
- content: "\F425";
-}
-.mdi-power-cycle::before {
- content: "\F900";
-}
-.mdi-power-off::before {
- content: "\F901";
-}
-.mdi-power-on::before {
- content: "\F902";
-}
-.mdi-power-plug::before {
- content: "\F6A4";
-}
-.mdi-power-plug-off::before {
- content: "\F6A5";
-}
-.mdi-power-settings::before {
- content: "\F426";
-}
-.mdi-power-sleep::before {
- content: "\F903";
-}
-.mdi-power-socket::before {
- content: "\F427";
-}
-.mdi-power-socket-au::before {
- content: "\F904";
-}
-.mdi-power-socket-de::before {
- content: "\F0132";
-}
-.mdi-power-socket-eu::before {
- content: "\F7E6";
-}
-.mdi-power-socket-fr::before {
- content: "\F0133";
-}
-.mdi-power-socket-jp::before {
- content: "\F0134";
-}
-.mdi-power-socket-uk::before {
- content: "\F7E7";
-}
-.mdi-power-socket-us::before {
- content: "\F7E8";
-}
-.mdi-power-standby::before {
- content: "\F905";
-}
-.mdi-powershell::before {
- content: "\FA09";
-}
-.mdi-prescription::before {
- content: "\F705";
-}
-.mdi-presentation::before {
- content: "\F428";
-}
-.mdi-presentation-play::before {
- content: "\F429";
-}
-.mdi-printer::before {
- content: "\F42A";
-}
-.mdi-printer-3d::before {
- content: "\F42B";
-}
-.mdi-printer-3d-nozzle::before {
- content: "\FE3E";
-}
-.mdi-printer-3d-nozzle-alert::before {
- content: "\F01EB";
-}
-.mdi-printer-3d-nozzle-alert-outline::before {
- content: "\F01EC";
-}
-.mdi-printer-3d-nozzle-outline::before {
- content: "\FE3F";
-}
-.mdi-printer-alert::before {
- content: "\F42C";
-}
-.mdi-printer-check::before {
- content: "\F0171";
-}
-.mdi-printer-off::before {
- content: "\FE40";
-}
-.mdi-printer-pos::before {
- content: "\F0079";
-}
-.mdi-printer-settings::before {
- content: "\F706";
-}
-.mdi-printer-wireless::before {
- content: "\FA0A";
-}
-.mdi-priority-high::before {
- content: "\F603";
-}
-.mdi-priority-low::before {
- content: "\F604";
-}
-.mdi-professional-hexagon::before {
- content: "\F42D";
-}
-.mdi-progress-alert::before {
- content: "\FC98";
-}
-.mdi-progress-check::before {
- content: "\F994";
-}
-.mdi-progress-clock::before {
- content: "\F995";
-}
-.mdi-progress-close::before {
- content: "\F0135";
-}
-.mdi-progress-download::before {
- content: "\F996";
-}
-.mdi-progress-upload::before {
- content: "\F997";
-}
-.mdi-progress-wrench::before {
- content: "\FC99";
-}
-.mdi-projector::before {
- content: "\F42E";
-}
-.mdi-projector-screen::before {
- content: "\F42F";
-}
-.mdi-propane-tank::before {
- content: "\F0382";
-}
-.mdi-propane-tank-outline::before {
- content: "\F0383";
-}
-.mdi-protocol::before {
- content: "\FFF9";
-}
-.mdi-publish::before {
- content: "\F6A6";
-}
-.mdi-pulse::before {
- content: "\F430";
-}
-.mdi-pumpkin::before {
- content: "\FB9B";
-}
-.mdi-purse::before {
- content: "\FF39";
-}
-.mdi-purse-outline::before {
- content: "\FF3A";
-}
-.mdi-puzzle::before {
- content: "\F431";
-}
-.mdi-puzzle-outline::before {
- content: "\FA65";
-}
-.mdi-qi::before {
- content: "\F998";
-}
-.mdi-qqchat::before {
- content: "\F605";
-}
-.mdi-qrcode::before {
- content: "\F432";
-}
-.mdi-qrcode-edit::before {
- content: "\F8B7";
-}
-.mdi-qrcode-minus::before {
- content: "\F01B7";
-}
-.mdi-qrcode-plus::before {
- content: "\F01B6";
-}
-.mdi-qrcode-remove::before {
- content: "\F01B8";
-}
-.mdi-qrcode-scan::before {
- content: "\F433";
-}
-.mdi-quadcopter::before {
- content: "\F434";
-}
-.mdi-quality-high::before {
- content: "\F435";
-}
-.mdi-quality-low::before {
- content: "\FA0B";
-}
-.mdi-quality-medium::before {
- content: "\FA0C";
-}
-.mdi-quicktime::before {
- content: "\F436";
-}
-.mdi-quora::before {
- content: "\FD05";
-}
-.mdi-rabbit::before {
- content: "\F906";
-}
-.mdi-racing-helmet::before {
- content: "\FD6F";
-}
-.mdi-racquetball::before {
- content: "\FD70";
-}
-.mdi-radar::before {
- content: "\F437";
-}
-.mdi-radiator::before {
- content: "\F438";
-}
-.mdi-radiator-disabled::before {
- content: "\FAD6";
-}
-.mdi-radiator-off::before {
- content: "\FAD7";
-}
-.mdi-radio::before {
- content: "\F439";
-}
-.mdi-radio-am::before {
- content: "\FC9A";
-}
-.mdi-radio-fm::before {
- content: "\FC9B";
-}
-.mdi-radio-handheld::before {
- content: "\F43A";
-}
-.mdi-radio-off::before {
- content: "\F0247";
-}
-.mdi-radio-tower::before {
- content: "\F43B";
-}
-.mdi-radioactive::before {
- content: "\F43C";
-}
-.mdi-radioactive-off::before {
- content: "\FEDE";
-}
-.mdi-radiobox-blank::before {
- content: "\F43D";
-}
-.mdi-radiobox-marked::before {
- content: "\F43E";
-}
-.mdi-radius::before {
- content: "\FC9C";
-}
-.mdi-radius-outline::before {
- content: "\FC9D";
-}
-.mdi-railroad-light::before {
- content: "\FF3B";
-}
-.mdi-raspberry-pi::before {
- content: "\F43F";
-}
-.mdi-ray-end::before {
- content: "\F440";
-}
-.mdi-ray-end-arrow::before {
- content: "\F441";
-}
-.mdi-ray-start::before {
- content: "\F442";
-}
-.mdi-ray-start-arrow::before {
- content: "\F443";
-}
-.mdi-ray-start-end::before {
- content: "\F444";
-}
-.mdi-ray-vertex::before {
- content: "\F445";
-}
-.mdi-react::before {
- content: "\F707";
-}
-.mdi-read::before {
- content: "\F447";
-}
-.mdi-receipt::before {
- content: "\F449";
-}
-.mdi-record::before {
- content: "\F44A";
-}
-.mdi-record-circle::before {
- content: "\FEDF";
-}
-.mdi-record-circle-outline::before {
- content: "\FEE0";
-}
-.mdi-record-player::before {
- content: "\F999";
-}
-.mdi-record-rec::before {
- content: "\F44B";
-}
-.mdi-rectangle::before {
- content: "\FE41";
-}
-.mdi-rectangle-outline::before {
- content: "\FE42";
-}
-.mdi-recycle::before {
- content: "\F44C";
-}
-.mdi-reddit::before {
- content: "\F44D";
-}
-.mdi-redhat::before {
- content: "\F0146";
-}
-.mdi-redo::before {
- content: "\F44E";
-}
-.mdi-redo-variant::before {
- content: "\F44F";
-}
-.mdi-reflect-horizontal::before {
- content: "\FA0D";
-}
-.mdi-reflect-vertical::before {
- content: "\FA0E";
-}
-.mdi-refresh::before {
- content: "\F450";
-}
-.mdi-refresh-circle::before {
- content: "\F03A2";
-}
-.mdi-regex::before {
- content: "\F451";
-}
-.mdi-registered-trademark::before {
- content: "\FA66";
-}
-.mdi-relative-scale::before {
- content: "\F452";
-}
-.mdi-reload::before {
- content: "\F453";
-}
-.mdi-reload-alert::before {
- content: "\F0136";
-}
-.mdi-reminder::before {
- content: "\F88B";
-}
-.mdi-remote::before {
- content: "\F454";
-}
-.mdi-remote-desktop::before {
- content: "\F8B8";
-}
-.mdi-remote-off::before {
- content: "\FEE1";
-}
-.mdi-remote-tv::before {
- content: "\FEE2";
-}
-.mdi-remote-tv-off::before {
- content: "\FEE3";
-}
-.mdi-rename-box::before {
- content: "\F455";
-}
-.mdi-reorder-horizontal::before {
- content: "\F687";
-}
-.mdi-reorder-vertical::before {
- content: "\F688";
-}
-.mdi-repeat::before {
- content: "\F456";
-}
-.mdi-repeat-off::before {
- content: "\F457";
-}
-.mdi-repeat-once::before {
- content: "\F458";
-}
-.mdi-replay::before {
- content: "\F459";
-}
-.mdi-reply::before {
- content: "\F45A";
-}
-.mdi-reply-all::before {
- content: "\F45B";
-}
-.mdi-reply-all-outline::before {
- content: "\FF3C";
-}
-.mdi-reply-circle::before {
- content: "\F01D9";
-}
-.mdi-reply-outline::before {
- content: "\FF3D";
-}
-.mdi-reproduction::before {
- content: "\F45C";
-}
-.mdi-resistor::before {
- content: "\FB1F";
-}
-.mdi-resistor-nodes::before {
- content: "\FB20";
-}
-.mdi-resize::before {
- content: "\FA67";
-}
-.mdi-resize-bottom-right::before {
- content: "\F45D";
-}
-.mdi-responsive::before {
- content: "\F45E";
-}
-.mdi-restart::before {
- content: "\F708";
-}
-.mdi-restart-alert::before {
- content: "\F0137";
-}
-.mdi-restart-off::before {
- content: "\FD71";
-}
-.mdi-restore::before {
- content: "\F99A";
-}
-.mdi-restore-alert::before {
- content: "\F0138";
-}
-.mdi-rewind::before {
- content: "\F45F";
-}
-.mdi-rewind-10::before {
- content: "\FD06";
-}
-.mdi-rewind-30::before {
- content: "\FD72";
-}
-.mdi-rewind-5::before {
- content: "\F0224";
-}
-.mdi-rewind-outline::before {
- content: "\F709";
-}
-.mdi-rhombus::before {
- content: "\F70A";
-}
-.mdi-rhombus-medium::before {
- content: "\FA0F";
-}
-.mdi-rhombus-outline::before {
- content: "\F70B";
-}
-.mdi-rhombus-split::before {
- content: "\FA10";
-}
-.mdi-ribbon::before {
- content: "\F460";
-}
-.mdi-rice::before {
- content: "\F7E9";
-}
-.mdi-ring::before {
- content: "\F7EA";
-}
-.mdi-rivet::before {
- content: "\FE43";
-}
-.mdi-road::before {
- content: "\F461";
-}
-.mdi-road-variant::before {
- content: "\F462";
-}
-.mdi-robber::before {
- content: "\F007A";
-}
-.mdi-robot::before {
- content: "\F6A8";
-}
-.mdi-robot-industrial::before {
- content: "\FB21";
-}
-.mdi-robot-mower::before {
- content: "\F0222";
-}
-.mdi-robot-mower-outline::before {
- content: "\F021E";
-}
-.mdi-robot-vacuum::before {
- content: "\F70C";
-}
-.mdi-robot-vacuum-variant::before {
- content: "\F907";
-}
-.mdi-rocket::before {
- content: "\F463";
-}
-.mdi-rodent::before {
- content: "\F0352";
-}
-.mdi-roller-skate::before {
- content: "\FD07";
-}
-.mdi-rollerblade::before {
- content: "\FD08";
-}
-.mdi-rollupjs::before {
- content: "\FB9C";
-}
-.mdi-roman-numeral-1::before {
- content: "\F00B3";
-}
-.mdi-roman-numeral-10::before {
- content: "\F00BC";
-}
-.mdi-roman-numeral-2::before {
- content: "\F00B4";
-}
-.mdi-roman-numeral-3::before {
- content: "\F00B5";
-}
-.mdi-roman-numeral-4::before {
- content: "\F00B6";
-}
-.mdi-roman-numeral-5::before {
- content: "\F00B7";
-}
-.mdi-roman-numeral-6::before {
- content: "\F00B8";
-}
-.mdi-roman-numeral-7::before {
- content: "\F00B9";
-}
-.mdi-roman-numeral-8::before {
- content: "\F00BA";
-}
-.mdi-roman-numeral-9::before {
- content: "\F00BB";
-}
-.mdi-room-service::before {
- content: "\F88C";
-}
-.mdi-room-service-outline::before {
- content: "\FD73";
-}
-.mdi-rotate-3d::before {
- content: "\FEE4";
-}
-.mdi-rotate-3d-variant::before {
- content: "\F464";
-}
-.mdi-rotate-left::before {
- content: "\F465";
-}
-.mdi-rotate-left-variant::before {
- content: "\F466";
-}
-.mdi-rotate-orbit::before {
- content: "\FD74";
-}
-.mdi-rotate-right::before {
- content: "\F467";
-}
-.mdi-rotate-right-variant::before {
- content: "\F468";
-}
-.mdi-rounded-corner::before {
- content: "\F607";
-}
-.mdi-router::before {
- content: "\F020D";
-}
-.mdi-router-wireless::before {
- content: "\F469";
-}
-.mdi-router-wireless-settings::before {
- content: "\FA68";
-}
-.mdi-routes::before {
- content: "\F46A";
-}
-.mdi-routes-clock::before {
- content: "\F007B";
-}
-.mdi-rowing::before {
- content: "\F608";
-}
-.mdi-rss::before {
- content: "\F46B";
-}
-.mdi-rss-box::before {
- content: "\F46C";
-}
-.mdi-rss-off::before {
- content: "\FF3E";
-}
-.mdi-ruby::before {
- content: "\FD09";
-}
-.mdi-rugby::before {
- content: "\FD75";
-}
-.mdi-ruler::before {
- content: "\F46D";
-}
-.mdi-ruler-square::before {
- content: "\FC9E";
-}
-.mdi-ruler-square-compass::before {
- content: "\FEDB";
-}
-.mdi-run::before {
- content: "\F70D";
-}
-.mdi-run-fast::before {
- content: "\F46E";
-}
-.mdi-rv-truck::before {
- content: "\F01FF";
-}
-.mdi-sack::before {
- content: "\FD0A";
-}
-.mdi-sack-percent::before {
- content: "\FD0B";
-}
-.mdi-safe::before {
- content: "\FA69";
-}
-.mdi-safe-square::before {
- content: "\F02A7";
-}
-.mdi-safe-square-outline::before {
- content: "\F02A8";
-}
-.mdi-safety-goggles::before {
- content: "\FD0C";
-}
-.mdi-sailing::before {
- content: "\FEE5";
-}
-.mdi-sale::before {
- content: "\F46F";
-}
-.mdi-salesforce::before {
- content: "\F88D";
-}
-.mdi-sass::before {
- content: "\F7EB";
-}
-.mdi-satellite::before {
- content: "\F470";
-}
-.mdi-satellite-uplink::before {
- content: "\F908";
-}
-.mdi-satellite-variant::before {
- content: "\F471";
-}
-.mdi-sausage::before {
- content: "\F8B9";
-}
-.mdi-saw-blade::before {
- content: "\FE44";
-}
-.mdi-saxophone::before {
- content: "\F609";
-}
-.mdi-scale::before {
- content: "\F472";
-}
-.mdi-scale-balance::before {
- content: "\F5D1";
-}
-.mdi-scale-bathroom::before {
- content: "\F473";
-}
-.mdi-scale-off::before {
- content: "\F007C";
-}
-.mdi-scanner::before {
- content: "\F6AA";
-}
-.mdi-scanner-off::before {
- content: "\F909";
-}
-.mdi-scatter-plot::before {
- content: "\FEE6";
-}
-.mdi-scatter-plot-outline::before {
- content: "\FEE7";
-}
-.mdi-school::before {
- content: "\F474";
-}
-.mdi-school-outline::before {
- content: "\F01AB";
-}
-.mdi-scissors-cutting::before {
- content: "\FA6A";
-}
-.mdi-scooter::before {
- content: "\F0214";
-}
-.mdi-scoreboard::before {
- content: "\F02A9";
-}
-.mdi-scoreboard-outline::before {
- content: "\F02AA";
-}
-.mdi-screen-rotation::before {
- content: "\F475";
-}
-.mdi-screen-rotation-lock::before {
- content: "\F476";
-}
-.mdi-screw-flat-top::before {
- content: "\FDCF";
-}
-.mdi-screw-lag::before {
- content: "\FE54";
-}
-.mdi-screw-machine-flat-top::before {
- content: "\FE55";
-}
-.mdi-screw-machine-round-top::before {
- content: "\FE56";
-}
-.mdi-screw-round-top::before {
- content: "\FE57";
-}
-.mdi-screwdriver::before {
- content: "\F477";
-}
-.mdi-script::before {
- content: "\FB9D";
-}
-.mdi-script-outline::before {
- content: "\F478";
-}
-.mdi-script-text::before {
- content: "\FB9E";
-}
-.mdi-script-text-outline::before {
- content: "\FB9F";
-}
-.mdi-sd::before {
- content: "\F479";
-}
-.mdi-seal::before {
- content: "\F47A";
-}
-.mdi-seal-variant::before {
- content: "\FFFA";
-}
-.mdi-search-web::before {
- content: "\F70E";
-}
-.mdi-seat::before {
- content: "\FC9F";
-}
-.mdi-seat-flat::before {
- content: "\F47B";
-}
-.mdi-seat-flat-angled::before {
- content: "\F47C";
-}
-.mdi-seat-individual-suite::before {
- content: "\F47D";
-}
-.mdi-seat-legroom-extra::before {
- content: "\F47E";
-}
-.mdi-seat-legroom-normal::before {
- content: "\F47F";
-}
-.mdi-seat-legroom-reduced::before {
- content: "\F480";
-}
-.mdi-seat-outline::before {
- content: "\FCA0";
-}
-.mdi-seat-passenger::before {
- content: "\F0274";
-}
-.mdi-seat-recline-extra::before {
- content: "\F481";
-}
-.mdi-seat-recline-normal::before {
- content: "\F482";
-}
-.mdi-seatbelt::before {
- content: "\FCA1";
-}
-.mdi-security::before {
- content: "\F483";
-}
-.mdi-security-network::before {
- content: "\F484";
-}
-.mdi-seed::before {
- content: "\FE45";
-}
-.mdi-seed-outline::before {
- content: "\FE46";
-}
-.mdi-segment::before {
- content: "\FEE8";
-}
-.mdi-select::before {
- content: "\F485";
-}
-.mdi-select-all::before {
- content: "\F486";
-}
-.mdi-select-color::before {
- content: "\FD0D";
-}
-.mdi-select-compare::before {
- content: "\FAD8";
-}
-.mdi-select-drag::before {
- content: "\FA6B";
-}
-.mdi-select-group::before {
- content: "\FF9F";
-}
-.mdi-select-inverse::before {
- content: "\F487";
-}
-.mdi-select-marker::before {
- content: "\F02AB";
-}
-.mdi-select-multiple::before {
- content: "\F02AC";
-}
-.mdi-select-multiple-marker::before {
- content: "\F02AD";
-}
-.mdi-select-off::before {
- content: "\F488";
-}
-.mdi-select-place::before {
- content: "\FFFB";
-}
-.mdi-select-search::before {
- content: "\F022F";
-}
-.mdi-selection::before {
- content: "\F489";
-}
-.mdi-selection-drag::before {
- content: "\FA6C";
-}
-.mdi-selection-ellipse::before {
- content: "\FD0E";
-}
-.mdi-selection-ellipse-arrow-inside::before {
- content: "\FF3F";
-}
-.mdi-selection-marker::before {
- content: "\F02AE";
-}
-.mdi-selection-multiple-marker::before {
- content: "\F02AF";
-}
-.mdi-selection-mutliple::before {
- content: "\F02B0";
-}
-.mdi-selection-off::before {
- content: "\F776";
-}
-.mdi-selection-search::before {
- content: "\F0230";
-}
-.mdi-semantic-web::before {
- content: "\F0341";
-}
-.mdi-send::before {
- content: "\F48A";
-}
-.mdi-send-check::before {
- content: "\F018C";
-}
-.mdi-send-check-outline::before {
- content: "\F018D";
-}
-.mdi-send-circle::before {
- content: "\FE58";
-}
-.mdi-send-circle-outline::before {
- content: "\FE59";
-}
-.mdi-send-clock::before {
- content: "\F018E";
-}
-.mdi-send-clock-outline::before {
- content: "\F018F";
-}
-.mdi-send-lock::before {
- content: "\F7EC";
-}
-.mdi-send-lock-outline::before {
- content: "\F0191";
-}
-.mdi-send-outline::before {
- content: "\F0190";
-}
-.mdi-serial-port::before {
- content: "\F65C";
-}
-.mdi-server::before {
- content: "\F48B";
-}
-.mdi-server-minus::before {
- content: "\F48C";
-}
-.mdi-server-network::before {
- content: "\F48D";
-}
-.mdi-server-network-off::before {
- content: "\F48E";
-}
-.mdi-server-off::before {
- content: "\F48F";
-}
-.mdi-server-plus::before {
- content: "\F490";
-}
-.mdi-server-remove::before {
- content: "\F491";
-}
-.mdi-server-security::before {
- content: "\F492";
-}
-.mdi-set-all::before {
- content: "\F777";
-}
-.mdi-set-center::before {
- content: "\F778";
-}
-.mdi-set-center-right::before {
- content: "\F779";
-}
-.mdi-set-left::before {
- content: "\F77A";
-}
-.mdi-set-left-center::before {
- content: "\F77B";
-}
-.mdi-set-left-right::before {
- content: "\F77C";
-}
-.mdi-set-none::before {
- content: "\F77D";
-}
-.mdi-set-right::before {
- content: "\F77E";
-}
-.mdi-set-top-box::before {
- content: "\F99E";
-}
-.mdi-settings::before {
- content: "\F493";
-}
-.mdi-settings-box::before {
- content: "\F494";
-}
-.mdi-settings-helper::before {
- content: "\FA6D";
-}
-.mdi-settings-outline::before {
- content: "\F8BA";
-}
-.mdi-settings-transfer::before {
- content: "\F007D";
-}
-.mdi-settings-transfer-outline::before {
- content: "\F007E";
-}
-.mdi-shaker::before {
- content: "\F0139";
-}
-.mdi-shaker-outline::before {
- content: "\F013A";
-}
-.mdi-shape::before {
- content: "\F830";
-}
-.mdi-shape-circle-plus::before {
- content: "\F65D";
-}
-.mdi-shape-outline::before {
- content: "\F831";
-}
-.mdi-shape-oval-plus::before {
- content: "\F0225";
-}
-.mdi-shape-plus::before {
- content: "\F495";
-}
-.mdi-shape-polygon-plus::before {
- content: "\F65E";
-}
-.mdi-shape-rectangle-plus::before {
- content: "\F65F";
-}
-.mdi-shape-square-plus::before {
- content: "\F660";
-}
-.mdi-share::before {
- content: "\F496";
-}
-.mdi-share-all::before {
- content: "\F021F";
-}
-.mdi-share-all-outline::before {
- content: "\F0220";
-}
-.mdi-share-circle::before {
- content: "\F01D8";
-}
-.mdi-share-off::before {
- content: "\FF40";
-}
-.mdi-share-off-outline::before {
- content: "\FF41";
-}
-.mdi-share-outline::before {
- content: "\F931";
-}
-.mdi-share-variant::before {
- content: "\F497";
-}
-.mdi-sheep::before {
- content: "\FCA2";
-}
-.mdi-shield::before {
- content: "\F498";
-}
-.mdi-shield-account::before {
- content: "\F88E";
-}
-.mdi-shield-account-outline::before {
- content: "\FA11";
-}
-.mdi-shield-airplane::before {
- content: "\F6BA";
-}
-.mdi-shield-airplane-outline::before {
- content: "\FCA3";
-}
-.mdi-shield-alert::before {
- content: "\FEE9";
-}
-.mdi-shield-alert-outline::before {
- content: "\FEEA";
-}
-.mdi-shield-car::before {
- content: "\FFA0";
-}
-.mdi-shield-check::before {
- content: "\F565";
-}
-.mdi-shield-check-outline::before {
- content: "\FCA4";
-}
-.mdi-shield-cross::before {
- content: "\FCA5";
-}
-.mdi-shield-cross-outline::before {
- content: "\FCA6";
-}
-.mdi-shield-edit::before {
- content: "\F01CB";
-}
-.mdi-shield-edit-outline::before {
- content: "\F01CC";
-}
-.mdi-shield-half::before {
- content: "\F038B";
-}
-.mdi-shield-half-full::before {
- content: "\F77F";
-}
-.mdi-shield-home::before {
- content: "\F689";
-}
-.mdi-shield-home-outline::before {
- content: "\FCA7";
-}
-.mdi-shield-key::before {
- content: "\FBA0";
-}
-.mdi-shield-key-outline::before {
- content: "\FBA1";
-}
-.mdi-shield-link-variant::before {
- content: "\FD0F";
-}
-.mdi-shield-link-variant-outline::before {
- content: "\FD10";
-}
-.mdi-shield-lock::before {
- content: "\F99C";
-}
-.mdi-shield-lock-outline::before {
- content: "\FCA8";
-}
-.mdi-shield-off::before {
- content: "\F99D";
-}
-.mdi-shield-off-outline::before {
- content: "\F99B";
-}
-.mdi-shield-outline::before {
- content: "\F499";
-}
-.mdi-shield-plus::before {
- content: "\FAD9";
-}
-.mdi-shield-plus-outline::before {
- content: "\FADA";
-}
-.mdi-shield-refresh::before {
- content: "\F01CD";
-}
-.mdi-shield-refresh-outline::before {
- content: "\F01CE";
-}
-.mdi-shield-remove::before {
- content: "\FADB";
-}
-.mdi-shield-remove-outline::before {
- content: "\FADC";
-}
-.mdi-shield-search::before {
- content: "\FD76";
-}
-.mdi-shield-star::before {
- content: "\F0166";
-}
-.mdi-shield-star-outline::before {
- content: "\F0167";
-}
-.mdi-shield-sun::before {
- content: "\F007F";
-}
-.mdi-shield-sun-outline::before {
- content: "\F0080";
-}
-.mdi-ship-wheel::before {
- content: "\F832";
-}
-.mdi-shoe-formal::before {
- content: "\FB22";
-}
-.mdi-shoe-heel::before {
- content: "\FB23";
-}
-.mdi-shoe-print::before {
- content: "\FE5A";
-}
-.mdi-shopify::before {
- content: "\FADD";
-}
-.mdi-shopping::before {
- content: "\F49A";
-}
-.mdi-shopping-music::before {
- content: "\F49B";
-}
-.mdi-shopping-outline::before {
- content: "\F0200";
-}
-.mdi-shopping-search::before {
- content: "\FFA1";
-}
-.mdi-shovel::before {
- content: "\F70F";
-}
-.mdi-shovel-off::before {
- content: "\F710";
-}
-.mdi-shower::before {
- content: "\F99F";
-}
-.mdi-shower-head::before {
- content: "\F9A0";
-}
-.mdi-shredder::before {
- content: "\F49C";
-}
-.mdi-shuffle::before {
- content: "\F49D";
-}
-.mdi-shuffle-disabled::before {
- content: "\F49E";
-}
-.mdi-shuffle-variant::before {
- content: "\F49F";
-}
-.mdi-shuriken::before {
- content: "\F03AA";
-}
-.mdi-sigma::before {
- content: "\F4A0";
-}
-.mdi-sigma-lower::before {
- content: "\F62B";
-}
-.mdi-sign-caution::before {
- content: "\F4A1";
-}
-.mdi-sign-direction::before {
- content: "\F780";
-}
-.mdi-sign-direction-minus::before {
- content: "\F0022";
-}
-.mdi-sign-direction-plus::before {
- content: "\FFFD";
-}
-.mdi-sign-direction-remove::before {
- content: "\FFFE";
-}
-.mdi-sign-real-estate::before {
- content: "\F0143";
-}
-.mdi-sign-text::before {
- content: "\F781";
-}
-.mdi-signal::before {
- content: "\F4A2";
-}
-.mdi-signal-2g::before {
- content: "\F711";
-}
-.mdi-signal-3g::before {
- content: "\F712";
-}
-.mdi-signal-4g::before {
- content: "\F713";
-}
-.mdi-signal-5g::before {
- content: "\FA6E";
-}
-.mdi-signal-cellular-1::before {
- content: "\F8BB";
-}
-.mdi-signal-cellular-2::before {
- content: "\F8BC";
-}
-.mdi-signal-cellular-3::before {
- content: "\F8BD";
-}
-.mdi-signal-cellular-outline::before {
- content: "\F8BE";
-}
-.mdi-signal-distance-variant::before {
- content: "\FE47";
-}
-.mdi-signal-hspa::before {
- content: "\F714";
-}
-.mdi-signal-hspa-plus::before {
- content: "\F715";
-}
-.mdi-signal-off::before {
- content: "\F782";
-}
-.mdi-signal-variant::before {
- content: "\F60A";
-}
-.mdi-signature::before {
- content: "\FE5B";
-}
-.mdi-signature-freehand::before {
- content: "\FE5C";
-}
-.mdi-signature-image::before {
- content: "\FE5D";
-}
-.mdi-signature-text::before {
- content: "\FE5E";
-}
-.mdi-silo::before {
- content: "\FB24";
-}
-.mdi-silverware::before {
- content: "\F4A3";
-}
-.mdi-silverware-clean::before {
- content: "\FFFF";
-}
-.mdi-silverware-fork::before {
- content: "\F4A4";
-}
-.mdi-silverware-fork-knife::before {
- content: "\FA6F";
-}
-.mdi-silverware-spoon::before {
- content: "\F4A5";
-}
-.mdi-silverware-variant::before {
- content: "\F4A6";
-}
-.mdi-sim::before {
- content: "\F4A7";
-}
-.mdi-sim-alert::before {
- content: "\F4A8";
-}
-.mdi-sim-off::before {
- content: "\F4A9";
-}
-.mdi-simple-icons::before {
- content: "\F0348";
-}
-.mdi-sina-weibo::before {
- content: "\FADE";
-}
-.mdi-sitemap::before {
- content: "\F4AA";
-}
-.mdi-skate::before {
- content: "\FD11";
-}
-.mdi-skew-less::before {
- content: "\FD12";
-}
-.mdi-skew-more::before {
- content: "\FD13";
-}
-.mdi-ski::before {
- content: "\F032F";
-}
-.mdi-ski-cross-country::before {
- content: "\F0330";
-}
-.mdi-ski-water::before {
- content: "\F0331";
-}
-.mdi-skip-backward::before {
- content: "\F4AB";
-}
-.mdi-skip-backward-outline::before {
- content: "\FF42";
-}
-.mdi-skip-forward::before {
- content: "\F4AC";
-}
-.mdi-skip-forward-outline::before {
- content: "\FF43";
-}
-.mdi-skip-next::before {
- content: "\F4AD";
-}
-.mdi-skip-next-circle::before {
- content: "\F661";
-}
-.mdi-skip-next-circle-outline::before {
- content: "\F662";
-}
-.mdi-skip-next-outline::before {
- content: "\FF44";
-}
-.mdi-skip-previous::before {
- content: "\F4AE";
-}
-.mdi-skip-previous-circle::before {
- content: "\F663";
-}
-.mdi-skip-previous-circle-outline::before {
- content: "\F664";
-}
-.mdi-skip-previous-outline::before {
- content: "\FF45";
-}
-.mdi-skull::before {
- content: "\F68B";
-}
-.mdi-skull-crossbones::before {
- content: "\FBA2";
-}
-.mdi-skull-crossbones-outline::before {
- content: "\FBA3";
-}
-.mdi-skull-outline::before {
- content: "\FBA4";
-}
-.mdi-skype::before {
- content: "\F4AF";
-}
-.mdi-skype-business::before {
- content: "\F4B0";
-}
-.mdi-slack::before {
- content: "\F4B1";
-}
-.mdi-slackware::before {
- content: "\F90A";
-}
-.mdi-slash-forward::before {
- content: "\F0000";
-}
-.mdi-slash-forward-box::before {
- content: "\F0001";
-}
-.mdi-sleep::before {
- content: "\F4B2";
-}
-.mdi-sleep-off::before {
- content: "\F4B3";
-}
-.mdi-slope-downhill::before {
- content: "\FE5F";
-}
-.mdi-slope-uphill::before {
- content: "\FE60";
-}
-.mdi-slot-machine::before {
- content: "\F013F";
-}
-.mdi-slot-machine-outline::before {
- content: "\F0140";
-}
-.mdi-smart-card::before {
- content: "\F00E8";
-}
-.mdi-smart-card-outline::before {
- content: "\F00E9";
-}
-.mdi-smart-card-reader::before {
- content: "\F00EA";
-}
-.mdi-smart-card-reader-outline::before {
- content: "\F00EB";
-}
-.mdi-smog::before {
- content: "\FA70";
-}
-.mdi-smoke-detector::before {
- content: "\F392";
-}
-.mdi-smoking::before {
- content: "\F4B4";
-}
-.mdi-smoking-off::before {
- content: "\F4B5";
-}
-.mdi-snapchat::before {
- content: "\F4B6";
-}
-.mdi-snowboard::before {
- content: "\F0332";
-}
-.mdi-snowflake::before {
- content: "\F716";
-}
-.mdi-snowflake-alert::before {
- content: "\FF46";
-}
-.mdi-snowflake-melt::before {
- content: "\F02F6";
-}
-.mdi-snowflake-variant::before {
- content: "\FF47";
-}
-.mdi-snowman::before {
- content: "\F4B7";
-}
-.mdi-soccer::before {
- content: "\F4B8";
-}
-.mdi-soccer-field::before {
- content: "\F833";
-}
-.mdi-sofa::before {
- content: "\F4B9";
-}
-.mdi-solar-panel::before {
- content: "\FD77";
-}
-.mdi-solar-panel-large::before {
- content: "\FD78";
-}
-.mdi-solar-power::before {
- content: "\FA71";
-}
-.mdi-soldering-iron::before {
- content: "\F00BD";
-}
-.mdi-solid::before {
- content: "\F68C";
-}
-.mdi-sort::before {
- content: "\F4BA";
-}
-.mdi-sort-alphabetical::before {
- content: "\F4BB";
-}
-.mdi-sort-alphabetical-ascending::before {
- content: "\F0173";
-}
-.mdi-sort-alphabetical-descending::before {
- content: "\F0174";
-}
-.mdi-sort-ascending::before {
- content: "\F4BC";
-}
-.mdi-sort-descending::before {
- content: "\F4BD";
-}
-.mdi-sort-numeric::before {
- content: "\F4BE";
-}
-.mdi-sort-variant::before {
- content: "\F4BF";
-}
-.mdi-sort-variant-lock::before {
- content: "\FCA9";
-}
-.mdi-sort-variant-lock-open::before {
- content: "\FCAA";
-}
-.mdi-sort-variant-remove::before {
- content: "\F0172";
-}
-.mdi-soundcloud::before {
- content: "\F4C0";
-}
-.mdi-source-branch::before {
- content: "\F62C";
-}
-.mdi-source-commit::before {
- content: "\F717";
-}
-.mdi-source-commit-end::before {
- content: "\F718";
-}
-.mdi-source-commit-end-local::before {
- content: "\F719";
-}
-.mdi-source-commit-local::before {
- content: "\F71A";
-}
-.mdi-source-commit-next-local::before {
- content: "\F71B";
-}
-.mdi-source-commit-start::before {
- content: "\F71C";
-}
-.mdi-source-commit-start-next-local::before {
- content: "\F71D";
-}
-.mdi-source-fork::before {
- content: "\F4C1";
-}
-.mdi-source-merge::before {
- content: "\F62D";
-}
-.mdi-source-pull::before {
- content: "\F4C2";
-}
-.mdi-source-repository::before {
- content: "\FCAB";
-}
-.mdi-source-repository-multiple::before {
- content: "\FCAC";
-}
-.mdi-soy-sauce::before {
- content: "\F7ED";
-}
-.mdi-spa::before {
- content: "\FCAD";
-}
-.mdi-spa-outline::before {
- content: "\FCAE";
-}
-.mdi-space-invaders::before {
- content: "\FBA5";
-}
-.mdi-space-station::before {
- content: "\F03AE";
-}
-.mdi-spade::before {
- content: "\FE48";
-}
-.mdi-speaker::before {
- content: "\F4C3";
-}
-.mdi-speaker-bluetooth::before {
- content: "\F9A1";
-}
-.mdi-speaker-multiple::before {
- content: "\FD14";
-}
-.mdi-speaker-off::before {
- content: "\F4C4";
-}
-.mdi-speaker-wireless::before {
- content: "\F71E";
-}
-.mdi-speedometer::before {
- content: "\F4C5";
-}
-.mdi-speedometer-medium::before {
- content: "\FFA2";
-}
-.mdi-speedometer-slow::before {
- content: "\FFA3";
-}
-.mdi-spellcheck::before {
- content: "\F4C6";
-}
-.mdi-spider::before {
- content: "\F0215";
-}
-.mdi-spider-thread::before {
- content: "\F0216";
-}
-.mdi-spider-web::before {
- content: "\FBA6";
-}
-.mdi-spotify::before {
- content: "\F4C7";
-}
-.mdi-spotlight::before {
- content: "\F4C8";
-}
-.mdi-spotlight-beam::before {
- content: "\F4C9";
-}
-.mdi-spray::before {
- content: "\F665";
-}
-.mdi-spray-bottle::before {
- content: "\FADF";
-}
-.mdi-sprinkler::before {
- content: "\F0081";
-}
-.mdi-sprinkler-variant::before {
- content: "\F0082";
-}
-.mdi-sprout::before {
- content: "\FE49";
-}
-.mdi-sprout-outline::before {
- content: "\FE4A";
-}
-.mdi-square::before {
- content: "\F763";
-}
-.mdi-square-edit-outline::before {
- content: "\F90B";
-}
-.mdi-square-inc::before {
- content: "\F4CA";
-}
-.mdi-square-inc-cash::before {
- content: "\F4CB";
-}
-.mdi-square-medium::before {
- content: "\FA12";
-}
-.mdi-square-medium-outline::before {
- content: "\FA13";
-}
-.mdi-square-off::before {
- content: "\F0319";
-}
-.mdi-square-off-outline::before {
- content: "\F031A";
-}
-.mdi-square-outline::before {
- content: "\F762";
-}
-.mdi-square-root::before {
- content: "\F783";
-}
-.mdi-square-root-box::before {
- content: "\F9A2";
-}
-.mdi-square-small::before {
- content: "\FA14";
-}
-.mdi-squeegee::before {
- content: "\FAE0";
-}
-.mdi-ssh::before {
- content: "\F8BF";
-}
-.mdi-stack-exchange::before {
- content: "\F60B";
-}
-.mdi-stack-overflow::before {
- content: "\F4CC";
-}
-.mdi-stackpath::before {
- content: "\F359";
-}
-.mdi-stadium::before {
- content: "\F001A";
-}
-.mdi-stadium-variant::before {
- content: "\F71F";
-}
-.mdi-stairs::before {
- content: "\F4CD";
-}
-.mdi-stairs-down::before {
- content: "\F02E9";
-}
-.mdi-stairs-up::before {
- content: "\F02E8";
-}
-.mdi-stamper::before {
- content: "\FD15";
-}
-.mdi-standard-definition::before {
- content: "\F7EE";
-}
-.mdi-star::before {
- content: "\F4CE";
-}
-.mdi-star-box::before {
- content: "\FA72";
-}
-.mdi-star-box-multiple::before {
- content: "\F02B1";
-}
-.mdi-star-box-multiple-outline::before {
- content: "\F02B2";
-}
-.mdi-star-box-outline::before {
- content: "\FA73";
-}
-.mdi-star-circle::before {
- content: "\F4CF";
-}
-.mdi-star-circle-outline::before {
- content: "\F9A3";
-}
-.mdi-star-face::before {
- content: "\F9A4";
-}
-.mdi-star-four-points::before {
- content: "\FAE1";
-}
-.mdi-star-four-points-outline::before {
- content: "\FAE2";
-}
-.mdi-star-half::before {
- content: "\F4D0";
-}
-.mdi-star-off::before {
- content: "\F4D1";
-}
-.mdi-star-outline::before {
- content: "\F4D2";
-}
-.mdi-star-three-points::before {
- content: "\FAE3";
-}
-.mdi-star-three-points-outline::before {
- content: "\FAE4";
-}
-.mdi-state-machine::before {
- content: "\F021A";
-}
-.mdi-steam::before {
- content: "\F4D3";
-}
-.mdi-steam-box::before {
- content: "\F90C";
-}
-.mdi-steering::before {
- content: "\F4D4";
-}
-.mdi-steering-off::before {
- content: "\F90D";
-}
-.mdi-step-backward::before {
- content: "\F4D5";
-}
-.mdi-step-backward-2::before {
- content: "\F4D6";
-}
-.mdi-step-forward::before {
- content: "\F4D7";
-}
-.mdi-step-forward-2::before {
- content: "\F4D8";
-}
-.mdi-stethoscope::before {
- content: "\F4D9";
-}
-.mdi-sticker::before {
- content: "\F038F";
-}
-.mdi-sticker-alert::before {
- content: "\F0390";
-}
-.mdi-sticker-alert-outline::before {
- content: "\F0391";
-}
-.mdi-sticker-check::before {
- content: "\F0392";
-}
-.mdi-sticker-check-outline::before {
- content: "\F0393";
-}
-.mdi-sticker-circle-outline::before {
- content: "\F5D0";
-}
-.mdi-sticker-emoji::before {
- content: "\F784";
-}
-.mdi-sticker-minus::before {
- content: "\F0394";
-}
-.mdi-sticker-minus-outline::before {
- content: "\F0395";
-}
-.mdi-sticker-outline::before {
- content: "\F0396";
-}
-.mdi-sticker-plus::before {
- content: "\F0397";
-}
-.mdi-sticker-plus-outline::before {
- content: "\F0398";
-}
-.mdi-sticker-remove::before {
- content: "\F0399";
-}
-.mdi-sticker-remove-outline::before {
- content: "\F039A";
-}
-.mdi-stocking::before {
- content: "\F4DA";
-}
-.mdi-stomach::before {
- content: "\F00BE";
-}
-.mdi-stop::before {
- content: "\F4DB";
-}
-.mdi-stop-circle::before {
- content: "\F666";
-}
-.mdi-stop-circle-outline::before {
- content: "\F667";
-}
-.mdi-store::before {
- content: "\F4DC";
-}
-.mdi-store-24-hour::before {
- content: "\F4DD";
-}
-.mdi-store-outline::before {
- content: "\F038C";
-}
-.mdi-storefront::before {
- content: "\F00EC";
-}
-.mdi-stove::before {
- content: "\F4DE";
-}
-.mdi-strategy::before {
- content: "\F0201";
-}
-.mdi-strava::before {
- content: "\FB25";
-}
-.mdi-stretch-to-page::before {
- content: "\FF48";
-}
-.mdi-stretch-to-page-outline::before {
- content: "\FF49";
-}
-.mdi-string-lights::before {
- content: "\F02E5";
-}
-.mdi-string-lights-off::before {
- content: "\F02E6";
-}
-.mdi-subdirectory-arrow-left::before {
- content: "\F60C";
-}
-.mdi-subdirectory-arrow-right::before {
- content: "\F60D";
-}
-.mdi-subtitles::before {
- content: "\FA15";
-}
-.mdi-subtitles-outline::before {
- content: "\FA16";
-}
-.mdi-subway::before {
- content: "\F6AB";
-}
-.mdi-subway-alert-variant::before {
- content: "\FD79";
-}
-.mdi-subway-variant::before {
- content: "\F4DF";
-}
-.mdi-summit::before {
- content: "\F785";
-}
-.mdi-sunglasses::before {
- content: "\F4E0";
-}
-.mdi-surround-sound::before {
- content: "\F5C5";
-}
-.mdi-surround-sound-2-0::before {
- content: "\F7EF";
-}
-.mdi-surround-sound-3-1::before {
- content: "\F7F0";
-}
-.mdi-surround-sound-5-1::before {
- content: "\F7F1";
-}
-.mdi-surround-sound-7-1::before {
- content: "\F7F2";
-}
-.mdi-svg::before {
- content: "\F720";
-}
-.mdi-swap-horizontal::before {
- content: "\F4E1";
-}
-.mdi-swap-horizontal-bold::before {
- content: "\FBA9";
-}
-.mdi-swap-horizontal-circle::before {
- content: "\F0002";
-}
-.mdi-swap-horizontal-circle-outline::before {
- content: "\F0003";
-}
-.mdi-swap-horizontal-variant::before {
- content: "\F8C0";
-}
-.mdi-swap-vertical::before {
- content: "\F4E2";
-}
-.mdi-swap-vertical-bold::before {
- content: "\FBAA";
-}
-.mdi-swap-vertical-circle::before {
- content: "\F0004";
-}
-.mdi-swap-vertical-circle-outline::before {
- content: "\F0005";
-}
-.mdi-swap-vertical-variant::before {
- content: "\F8C1";
-}
-.mdi-swim::before {
- content: "\F4E3";
-}
-.mdi-switch::before {
- content: "\F4E4";
-}
-.mdi-sword::before {
- content: "\F4E5";
-}
-.mdi-sword-cross::before {
- content: "\F786";
-}
-.mdi-syllabary-hangul::before {
- content: "\F035E";
-}
-.mdi-syllabary-hiragana::before {
- content: "\F035F";
-}
-.mdi-syllabary-katakana::before {
- content: "\F0360";
-}
-.mdi-syllabary-katakana-half-width::before {
- content: "\F0361";
-}
-.mdi-symfony::before {
- content: "\FAE5";
-}
-.mdi-sync::before {
- content: "\F4E6";
-}
-.mdi-sync-alert::before {
- content: "\F4E7";
-}
-.mdi-sync-circle::before {
- content: "\F03A3";
-}
-.mdi-sync-off::before {
- content: "\F4E8";
-}
-.mdi-tab::before {
- content: "\F4E9";
-}
-.mdi-tab-minus::before {
- content: "\FB26";
-}
-.mdi-tab-plus::before {
- content: "\F75B";
-}
-.mdi-tab-remove::before {
- content: "\FB27";
-}
-.mdi-tab-unselected::before {
- content: "\F4EA";
-}
-.mdi-table::before {
- content: "\F4EB";
-}
-.mdi-table-border::before {
- content: "\FA17";
-}
-.mdi-table-chair::before {
- content: "\F0083";
-}
-.mdi-table-column::before {
- content: "\F834";
-}
-.mdi-table-column-plus-after::before {
- content: "\F4EC";
-}
-.mdi-table-column-plus-before::before {
- content: "\F4ED";
-}
-.mdi-table-column-remove::before {
- content: "\F4EE";
-}
-.mdi-table-column-width::before {
- content: "\F4EF";
-}
-.mdi-table-edit::before {
- content: "\F4F0";
-}
-.mdi-table-eye::before {
- content: "\F00BF";
-}
-.mdi-table-headers-eye::before {
- content: "\F0248";
-}
-.mdi-table-headers-eye-off::before {
- content: "\F0249";
-}
-.mdi-table-large::before {
- content: "\F4F1";
-}
-.mdi-table-large-plus::before {
- content: "\FFA4";
-}
-.mdi-table-large-remove::before {
- content: "\FFA5";
-}
-.mdi-table-merge-cells::before {
- content: "\F9A5";
-}
-.mdi-table-of-contents::before {
- content: "\F835";
-}
-.mdi-table-plus::before {
- content: "\FA74";
-}
-.mdi-table-remove::before {
- content: "\FA75";
-}
-.mdi-table-row::before {
- content: "\F836";
-}
-.mdi-table-row-height::before {
- content: "\F4F2";
-}
-.mdi-table-row-plus-after::before {
- content: "\F4F3";
-}
-.mdi-table-row-plus-before::before {
- content: "\F4F4";
-}
-.mdi-table-row-remove::before {
- content: "\F4F5";
-}
-.mdi-table-search::before {
- content: "\F90E";
-}
-.mdi-table-settings::before {
- content: "\F837";
-}
-.mdi-table-tennis::before {
- content: "\FE4B";
-}
-.mdi-tablet::before {
- content: "\F4F6";
-}
-.mdi-tablet-android::before {
- content: "\F4F7";
-}
-.mdi-tablet-cellphone::before {
- content: "\F9A6";
-}
-.mdi-tablet-dashboard::before {
- content: "\FEEB";
-}
-.mdi-tablet-ipad::before {
- content: "\F4F8";
-}
-.mdi-taco::before {
- content: "\F761";
-}
-.mdi-tag::before {
- content: "\F4F9";
-}
-.mdi-tag-faces::before {
- content: "\F4FA";
-}
-.mdi-tag-heart::before {
- content: "\F68A";
-}
-.mdi-tag-heart-outline::before {
- content: "\FBAB";
-}
-.mdi-tag-minus::before {
- content: "\F90F";
-}
-.mdi-tag-minus-outline::before {
- content: "\F024A";
-}
-.mdi-tag-multiple::before {
- content: "\F4FB";
-}
-.mdi-tag-multiple-outline::before {
- content: "\F0322";
-}
-.mdi-tag-off::before {
- content: "\F024B";
-}
-.mdi-tag-off-outline::before {
- content: "\F024C";
-}
-.mdi-tag-outline::before {
- content: "\F4FC";
-}
-.mdi-tag-plus::before {
- content: "\F721";
-}
-.mdi-tag-plus-outline::before {
- content: "\F024D";
-}
-.mdi-tag-remove::before {
- content: "\F722";
-}
-.mdi-tag-remove-outline::before {
- content: "\F024E";
-}
-.mdi-tag-text::before {
- content: "\F024F";
-}
-.mdi-tag-text-outline::before {
- content: "\F4FD";
-}
-.mdi-tank::before {
- content: "\FD16";
-}
-.mdi-tanker-truck::before {
- content: "\F0006";
-}
-.mdi-tape-measure::before {
- content: "\FB28";
-}
-.mdi-target::before {
- content: "\F4FE";
-}
-.mdi-target-account::before {
- content: "\FBAC";
-}
-.mdi-target-variant::before {
- content: "\FA76";
-}
-.mdi-taxi::before {
- content: "\F4FF";
-}
-.mdi-tea::before {
- content: "\FD7A";
-}
-.mdi-tea-outline::before {
- content: "\FD7B";
-}
-.mdi-teach::before {
- content: "\F88F";
-}
-.mdi-teamviewer::before {
- content: "\F500";
-}
-.mdi-telegram::before {
- content: "\F501";
-}
-.mdi-telescope::before {
- content: "\FB29";
-}
-.mdi-television::before {
- content: "\F502";
-}
-.mdi-television-ambient-light::before {
- content: "\F0381";
-}
-.mdi-television-box::before {
- content: "\F838";
-}
-.mdi-television-classic::before {
- content: "\F7F3";
-}
-.mdi-television-classic-off::before {
- content: "\F839";
-}
-.mdi-television-clean::before {
- content: "\F013B";
-}
-.mdi-television-guide::before {
- content: "\F503";
-}
-.mdi-television-off::before {
- content: "\F83A";
-}
-.mdi-television-pause::before {
- content: "\FFA6";
-}
-.mdi-television-play::before {
- content: "\FEEC";
-}
-.mdi-television-stop::before {
- content: "\FFA7";
-}
-.mdi-temperature-celsius::before {
- content: "\F504";
-}
-.mdi-temperature-fahrenheit::before {
- content: "\F505";
-}
-.mdi-temperature-kelvin::before {
- content: "\F506";
-}
-.mdi-tennis::before {
- content: "\FD7C";
-}
-.mdi-tennis-ball::before {
- content: "\F507";
-}
-.mdi-tent::before {
- content: "\F508";
-}
-.mdi-terraform::before {
- content: "\F0084";
-}
-.mdi-terrain::before {
- content: "\F509";
-}
-.mdi-test-tube::before {
- content: "\F668";
-}
-.mdi-test-tube-empty::before {
- content: "\F910";
-}
-.mdi-test-tube-off::before {
- content: "\F911";
-}
-.mdi-text::before {
- content: "\F9A7";
-}
-.mdi-text-recognition::before {
- content: "\F0168";
-}
-.mdi-text-shadow::before {
- content: "\F669";
-}
-.mdi-text-short::before {
- content: "\F9A8";
-}
-.mdi-text-subject::before {
- content: "\F9A9";
-}
-.mdi-text-to-speech::before {
- content: "\F50A";
-}
-.mdi-text-to-speech-off::before {
- content: "\F50B";
-}
-.mdi-textarea::before {
- content: "\F00C0";
-}
-.mdi-textbox::before {
- content: "\F60E";
-}
-.mdi-textbox-lock::before {
- content: "\F0388";
-}
-.mdi-textbox-password::before {
- content: "\F7F4";
-}
-.mdi-texture::before {
- content: "\F50C";
-}
-.mdi-texture-box::before {
- content: "\F0007";
-}
-.mdi-theater::before {
- content: "\F50D";
-}
-.mdi-theme-light-dark::before {
- content: "\F50E";
-}
-.mdi-thermometer::before {
- content: "\F50F";
-}
-.mdi-thermometer-alert::before {
- content: "\FE61";
-}
-.mdi-thermometer-chevron-down::before {
- content: "\FE62";
-}
-.mdi-thermometer-chevron-up::before {
- content: "\FE63";
-}
-.mdi-thermometer-high::before {
- content: "\F00ED";
-}
-.mdi-thermometer-lines::before {
- content: "\F510";
-}
-.mdi-thermometer-low::before {
- content: "\F00EE";
-}
-.mdi-thermometer-minus::before {
- content: "\FE64";
-}
-.mdi-thermometer-plus::before {
- content: "\FE65";
-}
-.mdi-thermostat::before {
- content: "\F393";
-}
-.mdi-thermostat-box::before {
- content: "\F890";
-}
-.mdi-thought-bubble::before {
- content: "\F7F5";
-}
-.mdi-thought-bubble-outline::before {
- content: "\F7F6";
-}
-.mdi-thumb-down::before {
- content: "\F511";
-}
-.mdi-thumb-down-outline::before {
- content: "\F512";
-}
-.mdi-thumb-up::before {
- content: "\F513";
-}
-.mdi-thumb-up-outline::before {
- content: "\F514";
-}
-.mdi-thumbs-up-down::before {
- content: "\F515";
-}
-.mdi-ticket::before {
- content: "\F516";
-}
-.mdi-ticket-account::before {
- content: "\F517";
-}
-.mdi-ticket-confirmation::before {
- content: "\F518";
-}
-.mdi-ticket-outline::before {
- content: "\F912";
-}
-.mdi-ticket-percent::before {
- content: "\F723";
-}
-.mdi-tie::before {
- content: "\F519";
-}
-.mdi-tilde::before {
- content: "\F724";
-}
-.mdi-timelapse::before {
- content: "\F51A";
-}
-.mdi-timeline::before {
- content: "\FBAD";
-}
-.mdi-timeline-alert::before {
- content: "\FFB2";
-}
-.mdi-timeline-alert-outline::before {
- content: "\FFB5";
-}
-.mdi-timeline-clock::before {
- content: "\F0226";
-}
-.mdi-timeline-clock-outline::before {
- content: "\F0227";
-}
-.mdi-timeline-help::before {
- content: "\FFB6";
-}
-.mdi-timeline-help-outline::before {
- content: "\FFB7";
-}
-.mdi-timeline-outline::before {
- content: "\FBAE";
-}
-.mdi-timeline-plus::before {
- content: "\FFB3";
-}
-.mdi-timeline-plus-outline::before {
- content: "\FFB4";
-}
-.mdi-timeline-text::before {
- content: "\FBAF";
-}
-.mdi-timeline-text-outline::before {
- content: "\FBB0";
-}
-.mdi-timer::before {
- content: "\F51B";
-}
-.mdi-timer-10::before {
- content: "\F51C";
-}
-.mdi-timer-3::before {
- content: "\F51D";
-}
-.mdi-timer-off::before {
- content: "\F51E";
-}
-.mdi-timer-sand::before {
- content: "\F51F";
-}
-.mdi-timer-sand-empty::before {
- content: "\F6AC";
-}
-.mdi-timer-sand-full::before {
- content: "\F78B";
-}
-.mdi-timetable::before {
- content: "\F520";
-}
-.mdi-toaster::before {
- content: "\F0085";
-}
-.mdi-toaster-off::before {
- content: "\F01E2";
-}
-.mdi-toaster-oven::before {
- content: "\FCAF";
-}
-.mdi-toggle-switch::before {
- content: "\F521";
-}
-.mdi-toggle-switch-off::before {
- content: "\F522";
-}
-.mdi-toggle-switch-off-outline::before {
- content: "\FA18";
-}
-.mdi-toggle-switch-outline::before {
- content: "\FA19";
-}
-.mdi-toilet::before {
- content: "\F9AA";
-}
-.mdi-toolbox::before {
- content: "\F9AB";
-}
-.mdi-toolbox-outline::before {
- content: "\F9AC";
-}
-.mdi-tools::before {
- content: "\F0086";
-}
-.mdi-tooltip::before {
- content: "\F523";
-}
-.mdi-tooltip-account::before {
- content: "\F00C";
-}
-.mdi-tooltip-edit::before {
- content: "\F524";
-}
-.mdi-tooltip-edit-outline::before {
- content: "\F02F0";
-}
-.mdi-tooltip-image::before {
- content: "\F525";
-}
-.mdi-tooltip-image-outline::before {
- content: "\FBB1";
-}
-.mdi-tooltip-outline::before {
- content: "\F526";
-}
-.mdi-tooltip-plus::before {
- content: "\FBB2";
-}
-.mdi-tooltip-plus-outline::before {
- content: "\F527";
-}
-.mdi-tooltip-text::before {
- content: "\F528";
-}
-.mdi-tooltip-text-outline::before {
- content: "\FBB3";
-}
-.mdi-tooth::before {
- content: "\F8C2";
-}
-.mdi-tooth-outline::before {
- content: "\F529";
-}
-.mdi-toothbrush::before {
- content: "\F0154";
-}
-.mdi-toothbrush-electric::before {
- content: "\F0157";
-}
-.mdi-toothbrush-paste::before {
- content: "\F0155";
-}
-.mdi-tor::before {
- content: "\F52A";
-}
-.mdi-tortoise::before {
- content: "\FD17";
-}
-.mdi-toslink::before {
- content: "\F02E3";
-}
-.mdi-tournament::before {
- content: "\F9AD";
-}
-.mdi-tower-beach::before {
- content: "\F680";
-}
-.mdi-tower-fire::before {
- content: "\F681";
-}
-.mdi-towing::before {
- content: "\F83B";
-}
-.mdi-toy-brick::before {
- content: "\F02B3";
-}
-.mdi-toy-brick-marker::before {
- content: "\F02B4";
-}
-.mdi-toy-brick-marker-outline::before {
- content: "\F02B5";
-}
-.mdi-toy-brick-minus::before {
- content: "\F02B6";
-}
-.mdi-toy-brick-minus-outline::before {
- content: "\F02B7";
-}
-.mdi-toy-brick-outline::before {
- content: "\F02B8";
-}
-.mdi-toy-brick-plus::before {
- content: "\F02B9";
-}
-.mdi-toy-brick-plus-outline::before {
- content: "\F02BA";
-}
-.mdi-toy-brick-remove::before {
- content: "\F02BB";
-}
-.mdi-toy-brick-remove-outline::before {
- content: "\F02BC";
-}
-.mdi-toy-brick-search::before {
- content: "\F02BD";
-}
-.mdi-toy-brick-search-outline::before {
- content: "\F02BE";
-}
-.mdi-track-light::before {
- content: "\F913";
-}
-.mdi-trackpad::before {
- content: "\F7F7";
-}
-.mdi-trackpad-lock::before {
- content: "\F932";
-}
-.mdi-tractor::before {
- content: "\F891";
-}
-.mdi-trademark::before {
- content: "\FA77";
-}
-.mdi-traffic-cone::before {
- content: "\F03A7";
-}
-.mdi-traffic-light::before {
- content: "\F52B";
-}
-.mdi-train::before {
- content: "\F52C";
-}
-.mdi-train-car::before {
- content: "\FBB4";
-}
-.mdi-train-variant::before {
- content: "\F8C3";
-}
-.mdi-tram::before {
- content: "\F52D";
-}
-.mdi-tram-side::before {
- content: "\F0008";
-}
-.mdi-transcribe::before {
- content: "\F52E";
-}
-.mdi-transcribe-close::before {
- content: "\F52F";
-}
-.mdi-transfer::before {
- content: "\F0087";
-}
-.mdi-transfer-down::before {
- content: "\FD7D";
-}
-.mdi-transfer-left::before {
- content: "\FD7E";
-}
-.mdi-transfer-right::before {
- content: "\F530";
-}
-.mdi-transfer-up::before {
- content: "\FD7F";
-}
-.mdi-transit-connection::before {
- content: "\FD18";
-}
-.mdi-transit-connection-variant::before {
- content: "\FD19";
-}
-.mdi-transit-detour::before {
- content: "\FFA8";
-}
-.mdi-transit-transfer::before {
- content: "\F6AD";
-}
-.mdi-transition::before {
- content: "\F914";
-}
-.mdi-transition-masked::before {
- content: "\F915";
-}
-.mdi-translate::before {
- content: "\F5CA";
-}
-.mdi-translate-off::before {
- content: "\FE66";
-}
-.mdi-transmission-tower::before {
- content: "\FD1A";
-}
-.mdi-trash-can::before {
- content: "\FA78";
-}
-.mdi-trash-can-outline::before {
- content: "\FA79";
-}
-.mdi-tray::before {
- content: "\F02BF";
-}
-.mdi-tray-alert::before {
- content: "\F02C0";
-}
-.mdi-tray-full::before {
- content: "\F02C1";
-}
-.mdi-tray-minus::before {
- content: "\F02C2";
-}
-.mdi-tray-plus::before {
- content: "\F02C3";
-}
-.mdi-tray-remove::before {
- content: "\F02C4";
-}
-.mdi-treasure-chest::before {
- content: "\F725";
-}
-.mdi-tree::before {
- content: "\F531";
-}
-.mdi-tree-outline::before {
- content: "\FE4C";
-}
-.mdi-trello::before {
- content: "\F532";
-}
-.mdi-trending-down::before {
- content: "\F533";
-}
-.mdi-trending-neutral::before {
- content: "\F534";
-}
-.mdi-trending-up::before {
- content: "\F535";
-}
-.mdi-triangle::before {
- content: "\F536";
-}
-.mdi-triangle-outline::before {
- content: "\F537";
-}
-.mdi-triforce::before {
- content: "\FBB5";
-}
-.mdi-trophy::before {
- content: "\F538";
-}
-.mdi-trophy-award::before {
- content: "\F539";
-}
-.mdi-trophy-broken::before {
- content: "\FD80";
-}
-.mdi-trophy-outline::before {
- content: "\F53A";
-}
-.mdi-trophy-variant::before {
- content: "\F53B";
-}
-.mdi-trophy-variant-outline::before {
- content: "\F53C";
-}
-.mdi-truck::before {
- content: "\F53D";
-}
-.mdi-truck-check::before {
- content: "\FCB0";
-}
-.mdi-truck-check-outline::before {
- content: "\F02C5";
-}
-.mdi-truck-delivery::before {
- content: "\F53E";
-}
-.mdi-truck-delivery-outline::before {
- content: "\F02C6";
-}
-.mdi-truck-fast::before {
- content: "\F787";
-}
-.mdi-truck-fast-outline::before {
- content: "\F02C7";
-}
-.mdi-truck-outline::before {
- content: "\F02C8";
-}
-.mdi-truck-trailer::before {
- content: "\F726";
-}
-.mdi-trumpet::before {
- content: "\F00C1";
-}
-.mdi-tshirt-crew::before {
- content: "\FA7A";
-}
-.mdi-tshirt-crew-outline::before {
- content: "\F53F";
-}
-.mdi-tshirt-v::before {
- content: "\FA7B";
-}
-.mdi-tshirt-v-outline::before {
- content: "\F540";
-}
-.mdi-tumble-dryer::before {
- content: "\F916";
-}
-.mdi-tumble-dryer-alert::before {
- content: "\F01E5";
-}
-.mdi-tumble-dryer-off::before {
- content: "\F01E6";
-}
-.mdi-tumblr::before {
- content: "\F541";
-}
-.mdi-tumblr-box::before {
- content: "\F917";
-}
-.mdi-tumblr-reblog::before {
- content: "\F542";
-}
-.mdi-tune::before {
- content: "\F62E";
-}
-.mdi-tune-vertical::before {
- content: "\F66A";
-}
-.mdi-turnstile::before {
- content: "\FCB1";
-}
-.mdi-turnstile-outline::before {
- content: "\FCB2";
-}
-.mdi-turtle::before {
- content: "\FCB3";
-}
-.mdi-twitch::before {
- content: "\F543";
-}
-.mdi-twitter::before {
- content: "\F544";
-}
-.mdi-twitter-box::before {
- content: "\F545";
-}
-.mdi-twitter-circle::before {
- content: "\F546";
-}
-.mdi-twitter-retweet::before {
- content: "\F547";
-}
-.mdi-two-factor-authentication::before {
- content: "\F9AE";
-}
-.mdi-typewriter::before {
- content: "\FF4A";
-}
-.mdi-uber::before {
- content: "\F748";
-}
-.mdi-ubisoft::before {
- content: "\FBB6";
-}
-.mdi-ubuntu::before {
- content: "\F548";
-}
-.mdi-ufo::before {
- content: "\F00EF";
-}
-.mdi-ufo-outline::before {
- content: "\F00F0";
-}
-.mdi-ultra-high-definition::before {
- content: "\F7F8";
-}
-.mdi-umbraco::before {
- content: "\F549";
-}
-.mdi-umbrella::before {
- content: "\F54A";
-}
-.mdi-umbrella-closed::before {
- content: "\F9AF";
-}
-.mdi-umbrella-outline::before {
- content: "\F54B";
-}
-.mdi-undo::before {
- content: "\F54C";
-}
-.mdi-undo-variant::before {
- content: "\F54D";
-}
-.mdi-unfold-less-horizontal::before {
- content: "\F54E";
-}
-.mdi-unfold-less-vertical::before {
- content: "\F75F";
-}
-.mdi-unfold-more-horizontal::before {
- content: "\F54F";
-}
-.mdi-unfold-more-vertical::before {
- content: "\F760";
-}
-.mdi-ungroup::before {
- content: "\F550";
-}
-.mdi-unicode::before {
- content: "\FEED";
-}
-.mdi-unity::before {
- content: "\F6AE";
-}
-.mdi-unreal::before {
- content: "\F9B0";
-}
-.mdi-untappd::before {
- content: "\F551";
-}
-.mdi-update::before {
- content: "\F6AF";
-}
-.mdi-upload::before {
- content: "\F552";
-}
-.mdi-upload-lock::before {
- content: "\F039E";
-}
-.mdi-upload-lock-outline::before {
- content: "\F039F";
-}
-.mdi-upload-multiple::before {
- content: "\F83C";
-}
-.mdi-upload-network::before {
- content: "\F6F5";
-}
-.mdi-upload-network-outline::before {
- content: "\FCB4";
-}
-.mdi-upload-off::before {
- content: "\F00F1";
-}
-.mdi-upload-off-outline::before {
- content: "\F00F2";
-}
-.mdi-upload-outline::before {
- content: "\FE67";
-}
-.mdi-usb::before {
- content: "\F553";
-}
-.mdi-usb-flash-drive::before {
- content: "\F02C9";
-}
-.mdi-usb-flash-drive-outline::before {
- content: "\F02CA";
-}
-.mdi-usb-port::before {
- content: "\F021B";
-}
-.mdi-valve::before {
- content: "\F0088";
-}
-.mdi-valve-closed::before {
- content: "\F0089";
-}
-.mdi-valve-open::before {
- content: "\F008A";
-}
-.mdi-van-passenger::before {
- content: "\F7F9";
-}
-.mdi-van-utility::before {
- content: "\F7FA";
-}
-.mdi-vanish::before {
- content: "\F7FB";
-}
-.mdi-vanity-light::before {
- content: "\F020C";
-}
-.mdi-variable::before {
- content: "\FAE6";
-}
-.mdi-variable-box::before {
- content: "\F013C";
-}
-.mdi-vector-arrange-above::before {
- content: "\F554";
-}
-.mdi-vector-arrange-below::before {
- content: "\F555";
-}
-.mdi-vector-bezier::before {
- content: "\FAE7";
-}
-.mdi-vector-circle::before {
- content: "\F556";
-}
-.mdi-vector-circle-variant::before {
- content: "\F557";
-}
-.mdi-vector-combine::before {
- content: "\F558";
-}
-.mdi-vector-curve::before {
- content: "\F559";
-}
-.mdi-vector-difference::before {
- content: "\F55A";
-}
-.mdi-vector-difference-ab::before {
- content: "\F55B";
-}
-.mdi-vector-difference-ba::before {
- content: "\F55C";
-}
-.mdi-vector-ellipse::before {
- content: "\F892";
-}
-.mdi-vector-intersection::before {
- content: "\F55D";
-}
-.mdi-vector-line::before {
- content: "\F55E";
-}
-.mdi-vector-link::before {
- content: "\F0009";
-}
-.mdi-vector-point::before {
- content: "\F55F";
-}
-.mdi-vector-polygon::before {
- content: "\F560";
-}
-.mdi-vector-polyline::before {
- content: "\F561";
-}
-.mdi-vector-polyline-edit::before {
- content: "\F0250";
-}
-.mdi-vector-polyline-minus::before {
- content: "\F0251";
-}
-.mdi-vector-polyline-plus::before {
- content: "\F0252";
-}
-.mdi-vector-polyline-remove::before {
- content: "\F0253";
-}
-.mdi-vector-radius::before {
- content: "\F749";
-}
-.mdi-vector-rectangle::before {
- content: "\F5C6";
-}
-.mdi-vector-selection::before {
- content: "\F562";
-}
-.mdi-vector-square::before {
- content: "\F001";
-}
-.mdi-vector-triangle::before {
- content: "\F563";
-}
-.mdi-vector-union::before {
- content: "\F564";
-}
-.mdi-venmo::before {
- content: "\F578";
-}
-.mdi-vhs::before {
- content: "\FA1A";
-}
-.mdi-vibrate::before {
- content: "\F566";
-}
-.mdi-vibrate-off::before {
- content: "\FCB5";
-}
-.mdi-video::before {
- content: "\F567";
-}
-.mdi-video-3d::before {
- content: "\F7FC";
-}
-.mdi-video-3d-variant::before {
- content: "\FEEE";
-}
-.mdi-video-4k-box::before {
- content: "\F83D";
-}
-.mdi-video-account::before {
- content: "\F918";
-}
-.mdi-video-check::before {
- content: "\F008B";
-}
-.mdi-video-check-outline::before {
- content: "\F008C";
-}
-.mdi-video-image::before {
- content: "\F919";
-}
-.mdi-video-input-antenna::before {
- content: "\F83E";
-}
-.mdi-video-input-component::before {
- content: "\F83F";
-}
-.mdi-video-input-hdmi::before {
- content: "\F840";
-}
-.mdi-video-input-scart::before {
- content: "\FFA9";
-}
-.mdi-video-input-svideo::before {
- content: "\F841";
-}
-.mdi-video-minus::before {
- content: "\F9B1";
-}
-.mdi-video-off::before {
- content: "\F568";
-}
-.mdi-video-off-outline::before {
- content: "\FBB7";
-}
-.mdi-video-outline::before {
- content: "\FBB8";
-}
-.mdi-video-plus::before {
- content: "\F9B2";
-}
-.mdi-video-stabilization::before {
- content: "\F91A";
-}
-.mdi-video-switch::before {
- content: "\F569";
-}
-.mdi-video-vintage::before {
- content: "\FA1B";
-}
-.mdi-video-wireless::before {
- content: "\FEEF";
-}
-.mdi-video-wireless-outline::before {
- content: "\FEF0";
-}
-.mdi-view-agenda::before {
- content: "\F56A";
-}
-.mdi-view-agenda-outline::before {
- content: "\F0203";
-}
-.mdi-view-array::before {
- content: "\F56B";
-}
-.mdi-view-carousel::before {
- content: "\F56C";
-}
-.mdi-view-column::before {
- content: "\F56D";
-}
-.mdi-view-comfy::before {
- content: "\FE4D";
-}
-.mdi-view-compact::before {
- content: "\FE4E";
-}
-.mdi-view-compact-outline::before {
- content: "\FE4F";
-}
-.mdi-view-dashboard::before {
- content: "\F56E";
-}
-.mdi-view-dashboard-outline::before {
- content: "\FA1C";
-}
-.mdi-view-dashboard-variant::before {
- content: "\F842";
-}
-.mdi-view-day::before {
- content: "\F56F";
-}
-.mdi-view-grid::before {
- content: "\F570";
-}
-.mdi-view-grid-outline::before {
- content: "\F0204";
-}
-.mdi-view-grid-plus::before {
- content: "\FFAA";
-}
-.mdi-view-grid-plus-outline::before {
- content: "\F0205";
-}
-.mdi-view-headline::before {
- content: "\F571";
-}
-.mdi-view-list::before {
- content: "\F572";
-}
-.mdi-view-module::before {
- content: "\F573";
-}
-.mdi-view-parallel::before {
- content: "\F727";
-}
-.mdi-view-quilt::before {
- content: "\F574";
-}
-.mdi-view-sequential::before {
- content: "\F728";
-}
-.mdi-view-split-horizontal::before {
- content: "\FBA7";
-}
-.mdi-view-split-vertical::before {
- content: "\FBA8";
-}
-.mdi-view-stream::before {
- content: "\F575";
-}
-.mdi-view-week::before {
- content: "\F576";
-}
-.mdi-vimeo::before {
- content: "\F577";
-}
-.mdi-violin::before {
- content: "\F60F";
-}
-.mdi-virtual-reality::before {
- content: "\F893";
-}
-.mdi-visual-studio::before {
- content: "\F610";
-}
-.mdi-visual-studio-code::before {
- content: "\FA1D";
-}
-.mdi-vk::before {
- content: "\F579";
-}
-.mdi-vk-box::before {
- content: "\F57A";
-}
-.mdi-vk-circle::before {
- content: "\F57B";
-}
-.mdi-vlc::before {
- content: "\F57C";
-}
-.mdi-voice::before {
- content: "\F5CB";
-}
-.mdi-voice-off::before {
- content: "\FEF1";
-}
-.mdi-voicemail::before {
- content: "\F57D";
-}
-.mdi-volleyball::before {
- content: "\F9B3";
-}
-.mdi-volume-high::before {
- content: "\F57E";
-}
-.mdi-volume-low::before {
- content: "\F57F";
-}
-.mdi-volume-medium::before {
- content: "\F580";
-}
-.mdi-volume-minus::before {
- content: "\F75D";
-}
-.mdi-volume-mute::before {
- content: "\F75E";
-}
-.mdi-volume-off::before {
- content: "\F581";
-}
-.mdi-volume-plus::before {
- content: "\F75C";
-}
-.mdi-volume-source::before {
- content: "\F014B";
-}
-.mdi-volume-variant-off::before {
- content: "\FE68";
-}
-.mdi-volume-vibrate::before {
- content: "\F014C";
-}
-.mdi-vote::before {
- content: "\FA1E";
-}
-.mdi-vote-outline::before {
- content: "\FA1F";
-}
-.mdi-vpn::before {
- content: "\F582";
-}
-.mdi-vuejs::before {
- content: "\F843";
-}
-.mdi-vuetify::before {
- content: "\FE50";
-}
-.mdi-walk::before {
- content: "\F583";
-}
-.mdi-wall::before {
- content: "\F7FD";
-}
-.mdi-wall-sconce::before {
- content: "\F91B";
-}
-.mdi-wall-sconce-flat::before {
- content: "\F91C";
-}
-.mdi-wall-sconce-variant::before {
- content: "\F91D";
-}
-.mdi-wallet::before {
- content: "\F584";
-}
-.mdi-wallet-giftcard::before {
- content: "\F585";
-}
-.mdi-wallet-membership::before {
- content: "\F586";
-}
-.mdi-wallet-outline::before {
- content: "\FBB9";
-}
-.mdi-wallet-plus::before {
- content: "\FFAB";
-}
-.mdi-wallet-plus-outline::before {
- content: "\FFAC";
-}
-.mdi-wallet-travel::before {
- content: "\F587";
-}
-.mdi-wallpaper::before {
- content: "\FE69";
-}
-.mdi-wan::before {
- content: "\F588";
-}
-.mdi-wardrobe::before {
- content: "\FFAD";
-}
-.mdi-wardrobe-outline::before {
- content: "\FFAE";
-}
-.mdi-warehouse::before {
- content: "\FFBB";
-}
-.mdi-washing-machine::before {
- content: "\F729";
-}
-.mdi-washing-machine-alert::before {
- content: "\F01E7";
-}
-.mdi-washing-machine-off::before {
- content: "\F01E8";
-}
-.mdi-watch::before {
- content: "\F589";
-}
-.mdi-watch-export::before {
- content: "\F58A";
-}
-.mdi-watch-export-variant::before {
- content: "\F894";
-}
-.mdi-watch-import::before {
- content: "\F58B";
-}
-.mdi-watch-import-variant::before {
- content: "\F895";
-}
-.mdi-watch-variant::before {
- content: "\F896";
-}
-.mdi-watch-vibrate::before {
- content: "\F6B0";
-}
-.mdi-watch-vibrate-off::before {
- content: "\FCB6";
-}
-.mdi-water::before {
- content: "\F58C";
-}
-.mdi-water-boiler::before {
- content: "\FFAF";
-}
-.mdi-water-boiler-alert::before {
- content: "\F01DE";
-}
-.mdi-water-boiler-off::before {
- content: "\F01DF";
-}
-.mdi-water-off::before {
- content: "\F58D";
-}
-.mdi-water-outline::before {
- content: "\FE6A";
-}
-.mdi-water-percent::before {
- content: "\F58E";
-}
-.mdi-water-polo::before {
- content: "\F02CB";
-}
-.mdi-water-pump::before {
- content: "\F58F";
-}
-.mdi-water-pump-off::before {
- content: "\FFB0";
-}
-.mdi-water-well::before {
- content: "\F008D";
-}
-.mdi-water-well-outline::before {
- content: "\F008E";
-}
-.mdi-watermark::before {
- content: "\F612";
-}
-.mdi-wave::before {
- content: "\FF4B";
-}
-.mdi-waves::before {
- content: "\F78C";
-}
-.mdi-waze::before {
- content: "\FBBA";
-}
-.mdi-weather-cloudy::before {
- content: "\F590";
-}
-.mdi-weather-cloudy-alert::before {
- content: "\FF4C";
-}
-.mdi-weather-cloudy-arrow-right::before {
- content: "\FE51";
-}
-.mdi-weather-fog::before {
- content: "\F591";
-}
-.mdi-weather-hail::before {
- content: "\F592";
-}
-.mdi-weather-hazy::before {
- content: "\FF4D";
-}
-.mdi-weather-hurricane::before {
- content: "\F897";
-}
-.mdi-weather-lightning::before {
- content: "\F593";
-}
-.mdi-weather-lightning-rainy::before {
- content: "\F67D";
-}
-.mdi-weather-night::before {
- content: "\F594";
-}
-.mdi-weather-night-partly-cloudy::before {
- content: "\FF4E";
-}
-.mdi-weather-partly-cloudy::before {
- content: "\F595";
-}
-.mdi-weather-partly-lightning::before {
- content: "\FF4F";
-}
-.mdi-weather-partly-rainy::before {
- content: "\FF50";
-}
-.mdi-weather-partly-snowy::before {
- content: "\FF51";
-}
-.mdi-weather-partly-snowy-rainy::before {
- content: "\FF52";
-}
-.mdi-weather-pouring::before {
- content: "\F596";
-}
-.mdi-weather-rainy::before {
- content: "\F597";
-}
-.mdi-weather-snowy::before {
- content: "\F598";
-}
-.mdi-weather-snowy-heavy::before {
- content: "\FF53";
-}
-.mdi-weather-snowy-rainy::before {
- content: "\F67E";
-}
-.mdi-weather-sunny::before {
- content: "\F599";
-}
-.mdi-weather-sunny-alert::before {
- content: "\FF54";
-}
-.mdi-weather-sunset::before {
- content: "\F59A";
-}
-.mdi-weather-sunset-down::before {
- content: "\F59B";
-}
-.mdi-weather-sunset-up::before {
- content: "\F59C";
-}
-.mdi-weather-tornado::before {
- content: "\FF55";
-}
-.mdi-weather-windy::before {
- content: "\F59D";
-}
-.mdi-weather-windy-variant::before {
- content: "\F59E";
-}
-.mdi-web::before {
- content: "\F59F";
-}
-.mdi-web-box::before {
- content: "\FFB1";
-}
-.mdi-web-clock::before {
- content: "\F0275";
-}
-.mdi-webcam::before {
- content: "\F5A0";
-}
-.mdi-webhook::before {
- content: "\F62F";
-}
-.mdi-webpack::before {
- content: "\F72A";
-}
-.mdi-webrtc::before {
- content: "\F0273";
-}
-.mdi-wechat::before {
- content: "\F611";
-}
-.mdi-weight::before {
- content: "\F5A1";
-}
-.mdi-weight-gram::before {
- content: "\FD1B";
-}
-.mdi-weight-kilogram::before {
- content: "\F5A2";
-}
-.mdi-weight-lifter::before {
- content: "\F0188";
-}
-.mdi-weight-pound::before {
- content: "\F9B4";
-}
-.mdi-whatsapp::before {
- content: "\F5A3";
-}
-.mdi-wheelchair-accessibility::before {
- content: "\F5A4";
-}
-.mdi-whistle::before {
- content: "\F9B5";
-}
-.mdi-whistle-outline::before {
- content: "\F02E7";
-}
-.mdi-white-balance-auto::before {
- content: "\F5A5";
-}
-.mdi-white-balance-incandescent::before {
- content: "\F5A6";
-}
-.mdi-white-balance-iridescent::before {
- content: "\F5A7";
-}
-.mdi-white-balance-sunny::before {
- content: "\F5A8";
-}
-.mdi-widgets::before {
- content: "\F72B";
-}
-.mdi-widgets-outline::before {
- content: "\F0380";
-}
-.mdi-wifi::before {
- content: "\F5A9";
-}
-.mdi-wifi-off::before {
- content: "\F5AA";
-}
-.mdi-wifi-star::before {
- content: "\FE6B";
-}
-.mdi-wifi-strength-1::before {
- content: "\F91E";
-}
-.mdi-wifi-strength-1-alert::before {
- content: "\F91F";
-}
-.mdi-wifi-strength-1-lock::before {
- content: "\F920";
-}
-.mdi-wifi-strength-2::before {
- content: "\F921";
-}
-.mdi-wifi-strength-2-alert::before {
- content: "\F922";
-}
-.mdi-wifi-strength-2-lock::before {
- content: "\F923";
-}
-.mdi-wifi-strength-3::before {
- content: "\F924";
-}
-.mdi-wifi-strength-3-alert::before {
- content: "\F925";
-}
-.mdi-wifi-strength-3-lock::before {
- content: "\F926";
-}
-.mdi-wifi-strength-4::before {
- content: "\F927";
-}
-.mdi-wifi-strength-4-alert::before {
- content: "\F928";
-}
-.mdi-wifi-strength-4-lock::before {
- content: "\F929";
-}
-.mdi-wifi-strength-alert-outline::before {
- content: "\F92A";
-}
-.mdi-wifi-strength-lock-outline::before {
- content: "\F92B";
-}
-.mdi-wifi-strength-off::before {
- content: "\F92C";
-}
-.mdi-wifi-strength-off-outline::before {
- content: "\F92D";
-}
-.mdi-wifi-strength-outline::before {
- content: "\F92E";
-}
-.mdi-wii::before {
- content: "\F5AB";
-}
-.mdi-wiiu::before {
- content: "\F72C";
-}
-.mdi-wikipedia::before {
- content: "\F5AC";
-}
-.mdi-wind-turbine::before {
- content: "\FD81";
-}
-.mdi-window-close::before {
- content: "\F5AD";
-}
-.mdi-window-closed::before {
- content: "\F5AE";
-}
-.mdi-window-closed-variant::before {
- content: "\F0206";
-}
-.mdi-window-maximize::before {
- content: "\F5AF";
-}
-.mdi-window-minimize::before {
- content: "\F5B0";
-}
-.mdi-window-open::before {
- content: "\F5B1";
-}
-.mdi-window-open-variant::before {
- content: "\F0207";
-}
-.mdi-window-restore::before {
- content: "\F5B2";
-}
-.mdi-window-shutter::before {
- content: "\F0147";
-}
-.mdi-window-shutter-alert::before {
- content: "\F0148";
-}
-.mdi-window-shutter-open::before {
- content: "\F0149";
-}
-.mdi-windows::before {
- content: "\F5B3";
-}
-.mdi-windows-classic::before {
- content: "\FA20";
-}
-.mdi-wiper::before {
- content: "\FAE8";
-}
-.mdi-wiper-wash::before {
- content: "\FD82";
-}
-.mdi-wordpress::before {
- content: "\F5B4";
-}
-.mdi-worker::before {
- content: "\F5B5";
-}
-.mdi-wrap::before {
- content: "\F5B6";
-}
-.mdi-wrap-disabled::before {
- content: "\FBBB";
-}
-.mdi-wrench::before {
- content: "\F5B7";
-}
-.mdi-wrench-outline::before {
- content: "\FBBC";
-}
-.mdi-wunderlist::before {
- content: "\F5B8";
-}
-.mdi-xamarin::before {
- content: "\F844";
-}
-.mdi-xamarin-outline::before {
- content: "\F845";
-}
-.mdi-xaml::before {
- content: "\F673";
-}
-.mdi-xbox::before {
- content: "\F5B9";
-}
-.mdi-xbox-controller::before {
- content: "\F5BA";
-}
-.mdi-xbox-controller-battery-alert::before {
- content: "\F74A";
-}
-.mdi-xbox-controller-battery-charging::before {
- content: "\FA21";
-}
-.mdi-xbox-controller-battery-empty::before {
- content: "\F74B";
-}
-.mdi-xbox-controller-battery-full::before {
- content: "\F74C";
-}
-.mdi-xbox-controller-battery-low::before {
- content: "\F74D";
-}
-.mdi-xbox-controller-battery-medium::before {
- content: "\F74E";
-}
-.mdi-xbox-controller-battery-unknown::before {
- content: "\F74F";
-}
-.mdi-xbox-controller-menu::before {
- content: "\FE52";
-}
-.mdi-xbox-controller-off::before {
- content: "\F5BB";
-}
-.mdi-xbox-controller-view::before {
- content: "\FE53";
-}
-.mdi-xda::before {
- content: "\F5BC";
-}
-.mdi-xing::before {
- content: "\F5BD";
-}
-.mdi-xing-box::before {
- content: "\F5BE";
-}
-.mdi-xing-circle::before {
- content: "\F5BF";
-}
-.mdi-xml::before {
- content: "\F5C0";
-}
-.mdi-xmpp::before {
- content: "\F7FE";
-}
-.mdi-yahoo::before {
- content: "\FB2A";
-}
-.mdi-yammer::before {
- content: "\F788";
-}
-.mdi-yeast::before {
- content: "\F5C1";
-}
-.mdi-yelp::before {
- content: "\F5C2";
-}
-.mdi-yin-yang::before {
- content: "\F67F";
-}
-.mdi-yoga::before {
- content: "\F01A7";
-}
-.mdi-youtube::before {
- content: "\F5C3";
-}
-.mdi-youtube-creator-studio::before {
- content: "\F846";
-}
-.mdi-youtube-gaming::before {
- content: "\F847";
-}
-.mdi-youtube-subscription::before {
- content: "\FD1C";
-}
-.mdi-youtube-tv::before {
- content: "\F448";
-}
-.mdi-z-wave::before {
- content: "\FAE9";
-}
-.mdi-zend::before {
- content: "\FAEA";
-}
-.mdi-zigbee::before {
- content: "\FD1D";
-}
-.mdi-zip-box::before {
- content: "\F5C4";
-}
-.mdi-zip-box-outline::before {
- content: "\F001B";
-}
-.mdi-zip-disk::before {
- content: "\FA22";
-}
-.mdi-zodiac-aquarius::before {
- content: "\FA7C";
-}
-.mdi-zodiac-aries::before {
- content: "\FA7D";
-}
-.mdi-zodiac-cancer::before {
- content: "\FA7E";
-}
-.mdi-zodiac-capricorn::before {
- content: "\FA7F";
-}
-.mdi-zodiac-gemini::before {
- content: "\FA80";
-}
-.mdi-zodiac-leo::before {
- content: "\FA81";
-}
-.mdi-zodiac-libra::before {
- content: "\FA82";
-}
-.mdi-zodiac-pisces::before {
- content: "\FA83";
-}
-.mdi-zodiac-sagittarius::before {
- content: "\FA84";
-}
-.mdi-zodiac-scorpio::before {
- content: "\FA85";
-}
-.mdi-zodiac-taurus::before {
- content: "\FA86";
-}
-.mdi-zodiac-virgo::before {
- content: "\FA87";
-}
-.mdi-blank::before {
- content: "\F68C";
- visibility: hidden;
-}
-.mdi-18px.mdi-set,
-.mdi-18px.mdi:before {
- font-size: 18px;
-}
-.mdi-24px.mdi-set,
-.mdi-24px.mdi:before {
- font-size: 24px;
-}
-.mdi-36px.mdi-set,
-.mdi-36px.mdi:before {
- font-size: 36px;
-}
-.mdi-48px.mdi-set,
-.mdi-48px.mdi:before {
- font-size: 48px;
-}
-.mdi-dark:before {
- color: rgba(0, 0, 0, 0.54);
-}
-.mdi-dark.mdi-inactive:before {
- color: rgba(0, 0, 0, 0.26);
-}
-.mdi-light:before {
- color: #fff;
-}
-.mdi-light.mdi-inactive:before {
- color: rgba(255, 255, 255, 0.3);
-}
-.mdi-rotate-45:before {
- -webkit-transform: rotate(45deg);
- -ms-transform: rotate(45deg);
- transform: rotate(45deg);
-}
-.mdi-rotate-90:before {
- -webkit-transform: rotate(90deg);
- -ms-transform: rotate(90deg);
- transform: rotate(90deg);
-}
-.mdi-rotate-135:before {
- -webkit-transform: rotate(135deg);
- -ms-transform: rotate(135deg);
- transform: rotate(135deg);
-}
-.mdi-rotate-180:before {
- -webkit-transform: rotate(180deg);
- -ms-transform: rotate(180deg);
- transform: rotate(180deg);
-}
-.mdi-rotate-225:before {
- -webkit-transform: rotate(225deg);
- -ms-transform: rotate(225deg);
- transform: rotate(225deg);
-}
-.mdi-rotate-270:before {
- -webkit-transform: rotate(270deg);
- -ms-transform: rotate(270deg);
- transform: rotate(270deg);
-}
-.mdi-rotate-315:before {
- -webkit-transform: rotate(315deg);
- -ms-transform: rotate(315deg);
- transform: rotate(315deg);
-}
-.mdi-flip-h:before {
- -webkit-transform: scaleX(-1);
- transform: scaleX(-1);
- filter: FlipH;
- -ms-filter: "FlipH";
-}
-.mdi-flip-v:before {
- -webkit-transform: scaleY(-1);
- transform: scaleY(-1);
- filter: FlipV;
- -ms-filter: "FlipV";
-}
-.mdi-spin:before {
- -webkit-animation: mdi-spin 2s infinite linear;
- animation: mdi-spin 2s infinite linear;
-}
-@-webkit-keyframes mdi-spin {
- 0% {
- -webkit-transform: rotate(0deg);
- transform: rotate(0deg);
- }
- 100% {
- -webkit-transform: rotate(359deg);
- transform: rotate(359deg);
- }
-}
-@keyframes mdi-spin {
- 0% {
- -webkit-transform: rotate(0deg);
- transform: rotate(0deg);
- }
- 100% {
- -webkit-transform: rotate(359deg);
- transform: rotate(359deg);
- }
-}
-
-/*# sourceMappingURL=materialdesignicons.css.map */
diff --git a/packages/demobank-ui/src/scss/libs/_all.scss b/packages/demobank-ui/src/scss/libs/_all.scss
deleted file mode 100644
index d33f8acc4..000000000
--- a/packages/demobank-ui/src/scss/libs/_all.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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 "node_modules/bulma-radio/bulma-radio";
-// @import "node_modules/bulma-responsive-tables/bulma-responsive-tables";
-@import "node_modules/bulma-checkbox/bulma-checkbox";
-// @import "node_modules/bulma-switch-control/bulma-switch-control";
-// @import "node_modules/bulma-upload-control/bulma-upload-control";
-
-/* Bulma */
-@import "node_modules/bulma/bulma";
diff --git a/packages/demobank-ui/src/scss/main.css b/packages/demobank-ui/src/scss/main.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/demobank-ui/src/scss/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/demobank-ui/src/scss/main.scss b/packages/demobank-ui/src/scss/main.scss
deleted file mode 100644
index b9a46718f..000000000
--- a/packages/demobank-ui/src/scss/main.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-@use "pure";
-@use "bank";
-@use "demo";
-@use "toggle";
-@use "colors-bank";
diff --git a/packages/demobank-ui/src/scss/pure.scss b/packages/demobank-ui/src/scss/pure.scss
deleted file mode 100644
index 25a261a5f..000000000
--- a/packages/demobank-ui/src/scss/pure.scss
+++ /dev/null
@@ -1,1397 +0,0 @@
-/*!
-Pure v2.2.0
-Copyright 2013 Yahoo!
-Licensed under the BSD License.
-https://github.com/pure-css/pure/blob/master/LICENSE
-*/
-/*!
-normalize.css v | MIT License | https://necolas.github.io/normalize.css/
-Copyright (c) Nicolas Gallagher and Jonathan Neal
-*/
-/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
-
-/* Document
- ========================================================================== */
-
-/**
- * 1. Correct the line height in all browsers.
- * 2. Prevent adjustments of font size after orientation changes in iOS.
- */
-
-html {
- line-height: 1.15; /* 1 */
- -webkit-text-size-adjust: 100%; /* 2 */
-}
-
-/* Sections
- ========================================================================== */
-
-/**
- * Remove the margin in all browsers.
- */
-
-body {
- margin: 0;
-}
-
-/**
- * Render the `main` element consistently in IE.
- */
-
-main {
- display: block;
-}
-
-/**
- * Correct the font size and margin on `h1` elements within `section` and
- * `article` contexts in Chrome, Firefox, and Safari.
- */
-
-h1 {
- font-size: 2em;
- margin: 0.67em 0;
-}
-
-/* Grouping content
- ========================================================================== */
-
-/**
- * 1. Add the correct box sizing in Firefox.
- * 2. Show the overflow in Edge and IE.
- */
-
-hr {
- -webkit-box-sizing: content-box;
- box-sizing: content-box; /* 1 */
- height: 0; /* 1 */
- overflow: visible; /* 2 */
-}
-
-/**
- * 1. Correct the inheritance and scaling of font size in all browsers.
- * 2. Correct the odd `em` font sizing in all browsers.
- */
-
-pre {
- font-family: monospace, monospace; /* 1 */
- font-size: 1em; /* 2 */
-}
-
-/* Text-level semantics
- ========================================================================== */
-
-/**
- * Remove the gray background on active links in IE 10.
- */
-
-a {
- background-color: transparent;
-}
-
-/**
- * 1. Remove the bottom border in Chrome 57-
- * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
- */
-
-abbr[title] {
- border-bottom: none; /* 1 */
- text-decoration: underline; /* 2 */
- -webkit-text-decoration: underline dotted;
- text-decoration: underline dotted; /* 2 */
-}
-
-/**
- * Add the correct font weight in Chrome, Edge, and Safari.
- */
-
-b,
-strong {
- font-weight: bolder;
-}
-
-/**
- * 1. Correct the inheritance and scaling of font size in all browsers.
- * 2. Correct the odd `em` font sizing in all browsers.
- */
-
-code,
-kbd,
-samp {
- font-family: monospace, monospace; /* 1 */
- font-size: 1em; /* 2 */
-}
-
-/**
- * Add the correct font size in all browsers.
- */
-
-small {
- font-size: 80%;
-}
-
-/**
- * Prevent `sub` and `sup` elements from affecting the line height in
- * all browsers.
- */
-
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-sup {
- top: -0.5em;
-}
-
-/* Embedded content
- ========================================================================== */
-
-/**
- * Remove the border on images inside links in IE 10.
- */
-
-img {
- border-style: none;
-}
-
-/* Forms
- ========================================================================== */
-
-/**
- * 1. Change the font styles in all browsers.
- * 2. Remove the margin in Firefox and Safari.
- */
-
-button,
-input,
-optgroup,
-select,
-textarea {
- font-family: inherit; /* 1 */
- font-size: 100%; /* 1 */
- line-height: 1.15; /* 1 */
- margin: 0; /* 2 */
-}
-
-/**
- * Show the overflow in IE.
- * 1. Show the overflow in Edge.
- */
-
-button,
-input {
- /* 1 */
- overflow: visible;
-}
-
-/**
- * Remove the inheritance of text transform in Edge, Firefox, and IE.
- * 1. Remove the inheritance of text transform in Firefox.
- */
-
-button,
-select {
- /* 1 */
- text-transform: none;
-}
-
-/**
- * Correct the inability to style clickable types in iOS and Safari.
- */
-
-button,
-[type="button"],
-[type="reset"],
-[type="submit"] {
- -webkit-appearance: button;
-}
-
-/**
- * Remove the inner border and padding in Firefox.
- */
-
-button::-moz-focus-inner,
-[type="button"]::-moz-focus-inner,
-[type="reset"]::-moz-focus-inner,
-[type="submit"]::-moz-focus-inner {
- border-style: none;
- padding: 0;
-}
-
-/**
- * Restore the focus styles unset by the previous rule.
- */
-
-button:-moz-focusring,
-[type="button"]:-moz-focusring,
-[type="reset"]:-moz-focusring,
-[type="submit"]:-moz-focusring {
- outline: 1px dotted ButtonText;
-}
-
-/**
- * Correct the padding in Firefox.
- */
-
-fieldset {
- padding: 0.35em 0.75em 0.625em;
-}
-
-/**
- * 1. Correct the text wrapping in Edge and IE.
- * 2. Correct the color inheritance from `fieldset` elements in IE.
- * 3. Remove the padding so developers are not caught out when they zero out
- * `fieldset` elements in all browsers.
- */
-
-legend {
- -webkit-box-sizing: border-box;
- box-sizing: border-box; /* 1 */
- color: inherit; /* 2 */
- display: table; /* 1 */
- max-width: 100%; /* 1 */
- padding: 0; /* 3 */
- white-space: normal; /* 1 */
-}
-
-/**
- * Add the correct vertical alignment in Chrome, Firefox, and Opera.
- */
-
-progress {
- vertical-align: baseline;
-}
-
-/**
- * Remove the default vertical scrollbar in IE 10+.
- */
-
-textarea {
- overflow: auto;
-}
-
-/**
- * 1. Add the correct box sizing in IE 10.
- * 2. Remove the padding in IE 10.
- */
-
-[type="checkbox"],
-[type="radio"] {
- -webkit-box-sizing: border-box;
- box-sizing: border-box; /* 1 */
- padding: 0; /* 2 */
-}
-
-/**
- * Correct the cursor style of increment and decrement buttons in Chrome.
- */
-
-[type="number"]::-webkit-inner-spin-button,
-[type="number"]::-webkit-outer-spin-button {
- height: auto;
-}
-
-/**
- * 1. Correct the odd appearance in Chrome and Safari.
- * 2. Correct the outline style in Safari.
- */
-
-[type="search"] {
- -webkit-appearance: textfield; /* 1 */
- outline-offset: -2px; /* 2 */
-}
-
-/**
- * Remove the inner padding in Chrome and Safari on macOS.
- */
-
-[type="search"]::-webkit-search-decoration {
- -webkit-appearance: none;
-}
-
-/**
- * 1. Correct the inability to style clickable types in iOS and Safari.
- * 2. Change font properties to `inherit` in Safari.
- */
-
-::-webkit-file-upload-button {
- -webkit-appearance: button; /* 1 */
- font: inherit; /* 2 */
-}
-
-/* Interactive
- ========================================================================== */
-
-/*
- * Add the correct display in Edge, IE 10+, and Firefox.
- */
-
-details {
- display: block;
-}
-
-/*
- * Add the correct display in all browsers.
- */
-
-summary {
- display: list-item;
-}
-
-/* Misc
- ========================================================================== */
-
-/**
- * Add the correct display in IE 10+.
- */
-
-template {
- display: none;
-}
-
-/**
- * Add the correct display in IE 10.
- */
-
-[hidden] {
- display: none;
-}
-
-/*csslint important:false*/
-
-/* ==========================================================================
- Pure Base Extras
- ========================================================================== */
-
-/**
- * Extra rules that Pure adds on top of Normalize.css
- */
-
-html {
- font-family: sans-serif;
-}
-
-/**
- * Always hide an element when it has the `hidden` HTML attribute.
- */
-
-.hidden,
-[hidden] {
- display: none !important;
-}
-
-/**
- * Add this class to an image to make it fit within it's fluid parent wrapper while maintaining
- * aspect ratio.
- */
-.pure-img {
- max-width: 100%;
- height: auto;
- display: block;
-}
-
-/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/
-
-.pure-g {
- letter-spacing: -0.31em; /* Webkit: collapse white-space between units */
- text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */
-
- /*
- Sets the font stack to fonts known to work properly with the above letter
- and word spacings. See: https://github.com/pure-css/pure/issues/41/
-
- The following font stack makes Pure Grids work on all known environments.
-
- * FreeSans: Ships with many Linux distros, including Ubuntu
-
- * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and
- Arial to get picked up by the browser, even though neither is available
- in Chrome OS.
-
- * Droid Sans: Ships with all versions of Android.
-
- * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows.
- */
- font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
-
- /* Use flexbox when possible to avoid `letter-spacing` side-effects. */
- display: -webkit-box;
- display: -ms-flexbox;
- display: flex;
- -webkit-box-orient: horizontal;
- -webkit-box-direction: normal;
- -ms-flex-flow: row wrap;
- flex-flow: row wrap;
-
- /* Prevents distributing space between rows */
- -ms-flex-line-pack: start;
- align-content: flex-start;
-}
-
-/* IE10 display: -ms-flexbox (and display: flex in IE 11) does not work inside a table; fall back to block and rely on font hack */
-@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
- table .pure-g {
- display: block;
- }
-}
-
-/* Opera as of 12 on Windows needs word-spacing.
- The ".opera-only" selector is used to prevent actual prefocus styling
- and is not required in markup.
-*/
-.opera-only :-o-prefocus,
-.pure-g {
- word-spacing: -0.43em;
-}
-
-.pure-u {
- display: inline-block;
- letter-spacing: normal;
- word-spacing: normal;
- vertical-align: top;
- text-rendering: auto;
-}
-
-/*
-Resets the font family back to the OS/browser's default sans-serif font,
-this the same font stack that Normalize.css sets for the `body`.
-*/
-.pure-g [class*="pure-u"] {
- font-family: sans-serif;
-}
-
-.pure-u-1,
-.pure-u-1-1,
-.pure-u-1-2,
-.pure-u-1-3,
-.pure-u-2-3,
-.pure-u-1-4,
-.pure-u-3-4,
-.pure-u-1-5,
-.pure-u-2-5,
-.pure-u-3-5,
-.pure-u-4-5,
-.pure-u-5-5,
-.pure-u-1-6,
-.pure-u-5-6,
-.pure-u-1-8,
-.pure-u-3-8,
-.pure-u-5-8,
-.pure-u-7-8,
-.pure-u-1-12,
-.pure-u-5-12,
-.pure-u-7-12,
-.pure-u-11-12,
-.pure-u-1-24,
-.pure-u-2-24,
-.pure-u-3-24,
-.pure-u-4-24,
-.pure-u-5-24,
-.pure-u-6-24,
-.pure-u-7-24,
-.pure-u-8-24,
-.pure-u-9-24,
-.pure-u-10-24,
-.pure-u-11-24,
-.pure-u-12-24,
-.pure-u-13-24,
-.pure-u-14-24,
-.pure-u-15-24,
-.pure-u-16-24,
-.pure-u-17-24,
-.pure-u-18-24,
-.pure-u-19-24,
-.pure-u-20-24,
-.pure-u-21-24,
-.pure-u-22-24,
-.pure-u-23-24,
-.pure-u-24-24 {
- display: inline-block;
- letter-spacing: normal;
- word-spacing: normal;
- vertical-align: top;
- text-rendering: auto;
-}
-
-.pure-u-1-24 {
- width: 4.1667%;
-}
-
-.pure-u-1-12,
-.pure-u-2-24 {
- width: 8.3333%;
-}
-
-.pure-u-1-8,
-.pure-u-3-24 {
- width: 12.5%;
-}
-
-.pure-u-1-6,
-.pure-u-4-24 {
- width: 16.6667%;
-}
-
-.pure-u-1-5 {
- width: 20%;
-}
-
-.pure-u-5-24 {
- width: 20.8333%;
-}
-
-.pure-u-1-4,
-.pure-u-6-24 {
- width: 25%;
-}
-
-.pure-u-7-24 {
- width: 29.1667%;
-}
-
-.pure-u-1-3,
-.pure-u-8-24 {
- width: 33.3333%;
-}
-
-.pure-u-3-8,
-.pure-u-9-24 {
- width: 37.5%;
-}
-
-.pure-u-2-5 {
- width: 40%;
-}
-
-.pure-u-5-12,
-.pure-u-10-24 {
- width: 41.6667%;
-}
-
-.pure-u-11-24 {
- width: 45.8333%;
-}
-
-.pure-u-1-2,
-.pure-u-12-24 {
- width: 50%;
-}
-
-.pure-u-13-24 {
- width: 54.1667%;
-}
-
-.pure-u-7-12,
-.pure-u-14-24 {
- width: 58.3333%;
-}
-
-.pure-u-3-5 {
- width: 60%;
-}
-
-.pure-u-5-8,
-.pure-u-15-24 {
- width: 62.5%;
-}
-
-.pure-u-2-3,
-.pure-u-16-24 {
- width: 66.6667%;
-}
-
-.pure-u-17-24 {
- width: 70.8333%;
-}
-
-.pure-u-3-4,
-.pure-u-18-24 {
- width: 75%;
-}
-
-.pure-u-19-24 {
- width: 79.1667%;
-}
-
-.pure-u-4-5 {
- width: 80%;
-}
-
-.pure-u-5-6,
-.pure-u-20-24 {
- width: 83.3333%;
-}
-
-.pure-u-7-8,
-.pure-u-21-24 {
- width: 87.5%;
-}
-
-.pure-u-11-12,
-.pure-u-22-24 {
- width: 91.6667%;
-}
-
-.pure-u-23-24 {
- width: 95.8333%;
-}
-
-.pure-u-1,
-.pure-u-1-1,
-.pure-u-5-5,
-.pure-u-24-24 {
- width: 100%;
-}
-.pure-button {
- /* Structure */
- display: inline-block;
- line-height: normal;
- white-space: nowrap;
- vertical-align: middle;
- text-align: center;
- cursor: pointer;
- -webkit-user-drag: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-/* Firefox: Get rid of the inner focus border */
-.pure-button::-moz-focus-inner {
- padding: 0;
- border: 0;
-}
-
-/* Inherit .pure-g styles */
-.pure-button-group {
- letter-spacing: -0.31em; /* Webkit: collapse white-space between units */
- text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */
-}
-
-.opera-only :-o-prefocus,
-.pure-button-group {
- word-spacing: -0.43em;
-}
-
-.pure-button-group .pure-button {
- letter-spacing: normal;
- word-spacing: normal;
- vertical-align: top;
- text-rendering: auto;
-}
-
-/*csslint outline-none:false*/
-
-.pure-button {
- font-family: inherit;
- font-size: 100%;
- padding: 0.5em 1em;
- color: rgba(0, 0, 0, 0.8);
- border: none rgba(0, 0, 0, 0);
- background-color: #e6e6e6;
- text-decoration: none;
- border-radius: 2px;
-}
-
-.pure-button-hover,
-.pure-button:hover,
-.pure-button:focus {
- background-image: -webkit-gradient(
- linear,
- left top,
- left bottom,
- from(transparent),
- color-stop(40%, rgba(0, 0, 0, 0.05)),
- to(rgba(0, 0, 0, 0.1))
- );
- background-image: linear-gradient(
- transparent,
- rgba(0, 0, 0, 0.05) 40%,
- rgba(0, 0, 0, 0.1)
- );
-}
-.pure-button:focus {
- outline: 0;
-}
-.pure-button-active,
-.pure-button:active {
- -webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset,
- 0 0 6px rgba(0, 0, 0, 0.2) inset;
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset,
- 0 0 6px rgba(0, 0, 0, 0.2) inset;
- border-color: #000;
-}
-
-.pure-button[disabled],
-.pure-button-disabled,
-.pure-button-disabled:hover,
-.pure-button-disabled:focus,
-.pure-button-disabled:active {
- border: none;
- background-image: none;
- opacity: 0.4;
- cursor: not-allowed;
- -webkit-box-shadow: none;
- box-shadow: none;
- pointer-events: none;
-}
-
-.pure-button-hidden {
- display: none;
-}
-
-.pure-button-primary,
-.pure-button-selected,
-a.pure-button-primary,
-a.pure-button-selected {
- background-color: rgb(0, 120, 231);
- color: #fff;
-}
-
-/* Button Groups */
-.pure-button-group .pure-button {
- margin: 0;
- border-radius: 0;
- border-right: 1px solid rgba(0, 0, 0, 0.2);
-}
-
-.pure-button-group .pure-button:first-child {
- border-top-left-radius: 2px;
- border-bottom-left-radius: 2px;
-}
-.pure-button-group .pure-button:last-child {
- border-top-right-radius: 2px;
- border-bottom-right-radius: 2px;
- border-right: none;
-}
-
-/*csslint box-model:false*/
-/*
-Box-model set to false because we're setting a height on select elements, which
-also have border and padding. This is done because some browsers don't render
-the padding. We explicitly set the box-model for select elements to border-box,
-so we can ignore the csslint warning.
-*/
-
-.pure-form input[type="text"],
-.pure-form input[type="password"],
-.pure-form input[type="email"],
-.pure-form input[type="url"],
-.pure-form input[type="date"],
-.pure-form input[type="month"],
-.pure-form input[type="time"],
-.pure-form input[type="datetime"],
-.pure-form input[type="datetime-local"],
-.pure-form input[type="week"],
-.pure-form input[type="number"],
-.pure-form input[type="search"],
-.pure-form input[type="tel"],
-.pure-form input[type="color"],
-.pure-form select,
-.pure-form textarea {
- padding: 0.5em 0.6em;
- display: inline-block;
- border: 1px solid #ccc;
- -webkit-box-shadow: inset 0 1px 3px #ddd;
- box-shadow: inset 0 1px 3px #ddd;
- border-radius: 4px;
- vertical-align: middle;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-/*
-Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
-since IE8 won't execute CSS that contains a CSS3 selector.
-*/
-.pure-form input:not([type]) {
- padding: 0.5em 0.6em;
- display: inline-block;
- border: 1px solid #ccc;
- -webkit-box-shadow: inset 0 1px 3px #ddd;
- box-shadow: inset 0 1px 3px #ddd;
- border-radius: 4px;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-/* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */
-/* May be able to remove this tweak as color inputs become more standardized across browsers. */
-.pure-form input[type="color"] {
- padding: 0.2em 0.5em;
-}
-
-.pure-form input[type="text"]:focus,
-.pure-form input[type="password"]:focus,
-.pure-form input[type="email"]:focus,
-.pure-form input[type="url"]:focus,
-.pure-form input[type="date"]:focus,
-.pure-form input[type="month"]:focus,
-.pure-form input[type="time"]:focus,
-.pure-form input[type="datetime"]:focus,
-.pure-form input[type="datetime-local"]:focus,
-.pure-form input[type="week"]:focus,
-.pure-form input[type="number"]:focus,
-.pure-form input[type="search"]:focus,
-.pure-form input[type="tel"]:focus,
-.pure-form input[type="color"]:focus,
-.pure-form select:focus,
-.pure-form textarea:focus {
- outline: 0;
- border-color: #129fea;
-}
-
-/*
-Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
-since IE8 won't execute CSS that contains a CSS3 selector.
-*/
-.pure-form input:not([type]):focus {
- outline: 0;
- border-color: #129fea;
-}
-
-.pure-form input[type="file"]:focus,
-.pure-form input[type="radio"]:focus,
-.pure-form input[type="checkbox"]:focus {
- outline: thin solid #129fea;
- outline: 1px auto #129fea;
-}
-.pure-form .pure-checkbox,
-.pure-form .pure-radio {
- margin: 0.5em 0;
- display: block;
-}
-
-.pure-form input[type="text"][disabled],
-.pure-form input[type="password"][disabled],
-.pure-form input[type="email"][disabled],
-.pure-form input[type="url"][disabled],
-.pure-form input[type="date"][disabled],
-.pure-form input[type="month"][disabled],
-.pure-form input[type="time"][disabled],
-.pure-form input[type="datetime"][disabled],
-.pure-form input[type="datetime-local"][disabled],
-.pure-form input[type="week"][disabled],
-.pure-form input[type="number"][disabled],
-.pure-form input[type="search"][disabled],
-.pure-form input[type="tel"][disabled],
-.pure-form input[type="color"][disabled],
-.pure-form select[disabled],
-.pure-form textarea[disabled] {
- cursor: not-allowed;
- background-color: #eaeded;
- color: #cad2d3;
-}
-
-/*
-Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
-since IE8 won't execute CSS that contains a CSS3 selector.
-*/
-.pure-form input:not([type])[disabled] {
- cursor: not-allowed;
- background-color: #eaeded;
- color: #cad2d3;
-}
-.pure-form input[readonly],
-.pure-form select[readonly],
-.pure-form textarea[readonly] {
- background-color: #eee; /* menu hover bg color */
- color: #777; /* menu text color */
- border-color: #ccc;
-}
-
-/**
- * Even if we add novalidate property
- * in the form, the styles are applied for
- * invalid elements so we need to remove this styles
- *
- */
-// .pure-form input:focus:invalid,
-// .pure-form textarea:focus:invalid,
-// .pure-form select:focus:invalid {
-// color: #b94a48;
-// border-color: #e9322d;
-// }
-// .pure-form input[type="file"]:focus:invalid:focus,
-// .pure-form input[type="radio"]:focus:invalid:focus,
-// .pure-form input[type="checkbox"]:focus:invalid:focus {
-// outline-color: #e9322d;
-// }
-.pure-form select {
- /* Normalizes the height; padding is not sufficient. */
- height: 2.25em;
- border: 1px solid #ccc;
- background-color: white;
-}
-.pure-form select[multiple] {
- height: auto;
-}
-.pure-form label {
- margin: 0.5em 0 0.2em;
-}
-.pure-form fieldset {
- margin: 0;
- padding: 0.35em 0 0.75em;
- border: 0;
-}
-.pure-form legend {
- display: block;
- width: 100%;
- padding: 0.3em 0;
- margin-bottom: 0.3em;
- color: #333;
- border-bottom: 1px solid #e5e5e5;
-}
-
-.pure-form-stacked input[type="text"],
-.pure-form-stacked input[type="password"],
-.pure-form-stacked input[type="email"],
-.pure-form-stacked input[type="url"],
-.pure-form-stacked input[type="date"],
-.pure-form-stacked input[type="month"],
-.pure-form-stacked input[type="time"],
-.pure-form-stacked input[type="datetime"],
-.pure-form-stacked input[type="datetime-local"],
-.pure-form-stacked input[type="week"],
-.pure-form-stacked input[type="number"],
-.pure-form-stacked input[type="search"],
-.pure-form-stacked input[type="tel"],
-.pure-form-stacked input[type="color"],
-.pure-form-stacked input[type="file"],
-.pure-form-stacked select,
-.pure-form-stacked label,
-.pure-form-stacked textarea {
- display: block;
- margin: 0.25em 0;
-}
-
-/*
-Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
-since IE8 won't execute CSS that contains a CSS3 selector.
-*/
-.pure-form-stacked input:not([type]) {
- display: block;
- margin: 0.25em 0;
-}
-.pure-form-aligned input,
-.pure-form-aligned textarea,
-.pure-form-aligned select,
-.pure-form-message-inline {
- display: inline-block;
- vertical-align: middle;
-}
-.pure-form-aligned textarea {
- vertical-align: top;
-}
-
-/* Aligned Forms */
-.pure-form-aligned .pure-control-group {
- margin-bottom: 0.5em;
-}
-.pure-form-aligned .pure-control-group label {
- text-align: right;
- display: inline-block;
- vertical-align: middle;
- width: 10em;
- margin: 0 1em 0 0;
-}
-.pure-form-aligned .pure-controls {
- margin: 1.5em 0 0 11em;
-}
-
-/* Rounded Inputs */
-.pure-form input.pure-input-rounded,
-.pure-form .pure-input-rounded {
- border-radius: 2em;
- padding: 0.5em 1em;
-}
-
-/* Grouped Inputs */
-.pure-form .pure-group fieldset {
- margin-bottom: 10px;
-}
-.pure-form .pure-group input,
-.pure-form .pure-group textarea {
- display: block;
- padding: 10px;
- margin: 0 0 -1px;
- border-radius: 0;
- position: relative;
- top: -1px;
-}
-.pure-form .pure-group input:focus,
-.pure-form .pure-group textarea:focus {
- z-index: 3;
-}
-.pure-form .pure-group input:first-child,
-.pure-form .pure-group textarea:first-child {
- top: 1px;
- border-radius: 4px 4px 0 0;
- margin: 0;
-}
-.pure-form .pure-group input:first-child:last-child,
-.pure-form .pure-group textarea:first-child:last-child {
- top: 1px;
- border-radius: 4px;
- margin: 0;
-}
-.pure-form .pure-group input:last-child,
-.pure-form .pure-group textarea:last-child {
- top: -2px;
- border-radius: 0 0 4px 4px;
- margin: 0;
-}
-.pure-form .pure-group button {
- margin: 0.35em 0;
-}
-
-.pure-form .pure-input-1 {
- width: 100%;
-}
-.pure-form .pure-input-3-4 {
- width: 75%;
-}
-.pure-form .pure-input-2-3 {
- width: 66%;
-}
-.pure-form .pure-input-1-2 {
- width: 50%;
-}
-.pure-form .pure-input-1-3 {
- width: 33%;
-}
-.pure-form .pure-input-1-4 {
- width: 25%;
-}
-
-/* Inline help for forms */
-.pure-form-message-inline {
- display: inline-block;
- padding-left: 0.3em;
- color: #666;
- vertical-align: middle;
- font-size: 0.875em;
-}
-
-/* Block help for forms */
-.pure-form-message {
- display: block;
- color: #666;
- font-size: 0.875em;
-}
-
-@media only screen and (max-width: 480px) {
- // .pure-form button[type="submit"] {
- // margin: 0.7em 0 0;
- // }
-
- // .pure-form input:not([type]),
- // .pure-form input[type="text"],
- // .pure-form input[type="password"],
- // .pure-form input[type="email"],
- // .pure-form input[type="url"],
- // .pure-form input[type="date"],
- // .pure-form input[type="month"],
- // .pure-form input[type="time"],
- // .pure-form input[type="datetime"],
- // .pure-form input[type="datetime-local"],
- // .pure-form input[type="week"],
- // .pure-form input[type="number"],
- // .pure-form input[type="search"],
- // .pure-form input[type="tel"],
- // .pure-form input[type="color"],
- // .pure-form label {
- // margin-bottom: 0.3em;
- // display: block;
- // }
-
- .pure-group input:not([type]),
- .pure-group input[type="text"],
- .pure-group input[type="password"],
- .pure-group input[type="email"],
- .pure-group input[type="url"],
- .pure-group input[type="date"],
- .pure-group input[type="month"],
- .pure-group input[type="time"],
- .pure-group input[type="datetime"],
- .pure-group input[type="datetime-local"],
- .pure-group input[type="week"],
- .pure-group input[type="number"],
- .pure-group input[type="search"],
- .pure-group input[type="tel"],
- .pure-group input[type="color"] {
- margin-bottom: 0;
- }
-
- .pure-form-aligned .pure-control-group label {
- margin-bottom: 0.3em;
- text-align: left;
- display: block;
- width: 100%;
- }
-
- .pure-form-aligned .pure-controls {
- margin: 1.5em 0 0 0;
- }
-
- .pure-form-message-inline,
- .pure-form-message {
- display: block;
- font-size: 0.75em;
- /* Increased bottom padding to make it group with its related input element. */
- padding: 0.2em 0 0.8em;
- }
-}
-
-/*csslint adjoining-classes: false, box-model:false*/
-.pure-menu {
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-.pure-menu-fixed {
- position: fixed;
- left: 0;
- top: 0;
- z-index: 3;
-}
-
-.pure-menu-list,
-.pure-menu-item {
- position: relative;
-}
-
-.pure-menu-list {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-.pure-menu-item {
- padding: 0;
- margin: 0;
- height: 100%;
-}
-
-.pure-menu-link,
-.pure-menu-heading {
- display: block;
- text-decoration: none;
- white-space: nowrap;
-}
-
-/* HORIZONTAL MENU */
-.pure-menu-horizontal {
- width: 100%;
- white-space: nowrap;
-}
-
-.pure-menu-horizontal .pure-menu-list {
- display: inline-block;
-}
-
-/* Initial menus should be inline-block so that they are horizontal */
-.pure-menu-horizontal .pure-menu-item,
-.pure-menu-horizontal .pure-menu-heading,
-.pure-menu-horizontal .pure-menu-separator {
- display: inline-block;
- vertical-align: middle;
-}
-
-/* Submenus should still be display: block; */
-.pure-menu-item .pure-menu-item {
- display: block;
-}
-
-.pure-menu-children {
- display: none;
- position: absolute;
- left: 100%;
- top: 0;
- margin: 0;
- padding: 0;
- z-index: 3;
-}
-
-.pure-menu-horizontal .pure-menu-children {
- left: 0;
- top: auto;
- width: inherit;
-}
-
-.pure-menu-allow-hover:hover > .pure-menu-children,
-.pure-menu-active > .pure-menu-children {
- display: block;
- position: absolute;
-}
-
-/* Vertical Menus - show the dropdown arrow */
-.pure-menu-has-children > .pure-menu-link:after {
- padding-left: 0.5em;
- content: "\25B8";
- font-size: small;
-}
-
-/* Horizontal Menus - show the dropdown arrow */
-.pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after {
- content: "\25BE";
-}
-
-/* scrollable menus */
-.pure-menu-scrollable {
- overflow-y: scroll;
- overflow-x: hidden;
-}
-
-.pure-menu-scrollable .pure-menu-list {
- display: block;
-}
-
-.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list {
- display: inline-block;
-}
-
-.pure-menu-horizontal.pure-menu-scrollable {
- white-space: nowrap;
- overflow-y: hidden;
- overflow-x: auto;
- /* a little extra padding for this style to allow for scrollbars */
- padding: 0.5em 0;
-}
-
-/* misc default styling */
-
-.pure-menu-separator,
-.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
- background-color: #ccc;
- height: 1px;
- margin: 0.3em 0;
-}
-
-.pure-menu-horizontal .pure-menu-separator {
- width: 1px;
- height: 1.3em;
- margin: 0 0.3em;
-}
-
-/* Need to reset the separator since submenu is vertical */
-.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
- display: block;
- width: auto;
-}
-
-.pure-menu-heading {
- text-transform: uppercase;
- color: #565d64;
-}
-
-.pure-menu-link {
- color: #777;
-}
-
-.pure-menu-children {
- background-color: #fff;
-}
-
-.pure-menu-link,
-.pure-menu-heading {
- padding: 0.5em 1em;
-}
-
-.pure-menu-disabled {
- opacity: 0.5;
-}
-
-.pure-menu-disabled .pure-menu-link:hover {
- background-color: transparent;
- cursor: default;
-}
-
-.pure-menu-active > .pure-menu-link,
-.pure-menu-link:hover,
-.pure-menu-link:focus {
- background-color: #eee;
-}
-
-.pure-menu-selected > .pure-menu-link,
-.pure-menu-selected > .pure-menu-link:visited {
- color: #000;
-}
-
-.pure-table {
- /* Remove spacing between table cells (from Normalize.css) */
- border-collapse: collapse;
- border-spacing: 0;
- empty-cells: show;
- border: 1px solid #cbcbcb;
-}
-
-.pure-table caption {
- color: #000;
- font: italic 85%/1 arial, sans-serif;
- padding: 1em 0;
- text-align: center;
-}
-
-.pure-table td,
-.pure-table th {
- border-left: 1px solid #cbcbcb; /* inner column border */
- border-width: 0 0 0 1px;
- font-size: inherit;
- margin: 0;
- overflow: visible; /*to make ths where the title is really long work*/
- padding: 0.5em 1em; /* cell padding */
-}
-
-.pure-table thead {
- background-color: #e0e0e0;
- color: #000;
- text-align: left;
- vertical-align: bottom;
-}
-
-/*
-striping:
- even - #fff (white)
- odd - #f2f2f2 (light gray)
-*/
-.pure-table td {
- background-color: transparent;
-}
-.pure-table-odd td {
- background-color: #f2f2f2;
-}
-
-/* nth-child selector for modern browsers */
-.pure-table-striped tr:nth-child(2n-1) td {
- background-color: #f2f2f2;
-}
-
-/* BORDERED TABLES */
-.pure-table-bordered td {
- border-bottom: 1px solid #cbcbcb;
-}
-.pure-table-bordered tbody > tr:last-child > td {
- border-bottom-width: 0;
-}
-
-/* HORIZONTAL BORDERED TABLES */
-
-.pure-table-horizontal td,
-.pure-table-horizontal th {
- border-width: 0 0 1px 0;
- border-bottom: 1px solid #cbcbcb;
-}
-.pure-table-horizontal tbody > tr:last-child > td {
- border-bottom-width: 0;
-}
diff --git a/packages/demobank-ui/src/scss/toggle.scss b/packages/demobank-ui/src/scss/toggle.scss
deleted file mode 100644
index 24636da2f..000000000
--- a/packages/demobank-ui/src/scss/toggle.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-$green: #56c080;
-
-.toggle {
- cursor: pointer;
- display: inline-block;
-}
-.toggle-switch {
- display: inline-block;
- background: #ccc;
- border-radius: 16px;
- width: 58px;
- height: 32px;
- position: relative;
- vertical-align: middle;
- transition: background 0.25s;
- &:before,
- &:after {
- content: "";
- }
- &:before {
- display: block;
- background: linear-gradient(to bottom, #fff 0%, #eee 100%);
- border-radius: 50%;
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
- width: 24px;
- height: 24px;
- position: absolute;
- top: 4px;
- left: 4px;
- transition: left 0.25s;
- }
- .toggle:hover &:before {
- background: linear-gradient(to bottom, #fff 0%, #fff 100%);
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
- }
- .toggle-checkbox:checked + & {
- background: $green;
- &:before {
- left: 30px;
- }
- }
-}
-.toggle-checkbox {
- position: absolute;
- visibility: hidden;
-}
-.toggle-label {
- margin-left: 5px;
- position: relative;
- top: 2px;
-}
diff --git a/packages/demobank-ui/src/settings.ts b/packages/demobank-ui/src/settings.ts
index 6c78d287b..44a016de6 100644
--- a/packages/demobank-ui/src/settings.ts
+++ b/packages/demobank-ui/src/settings.ts
@@ -15,11 +15,14 @@
*/
export interface BankUiSettings {
- backendBaseURL: string;
- allowRegistrations: boolean;
- showDemoNav: boolean;
- bankName: string;
- demoSites: [string, string][];
+ backendBaseURL?: string;
+ allowRegistrations?: boolean;
+ iconLinkURL?: string;
+ showDemoNav?: boolean;
+ simplePasswordForRandomAccounts?: boolean;
+ allowRandomAccountCreation?: boolean;
+ bankName?: string;
+ demoSites?: [string, string][];
}
/**
@@ -27,9 +30,12 @@ export interface BankUiSettings {
*/
const defaultSettings: BankUiSettings = {
backendBaseURL: "https://bank.demo.taler.net/demobanks/default/",
+ iconLinkURL: "https://demo.taler.net/",
allowRegistrations: true,
bankName: "Taler Bank",
showDemoNav: true,
+ simplePasswordForRandomAccounts: true,
+ allowRandomAccountCreation: true,
demoSites: [
["Landing", "https://demo.taler.net/"],
["Bank", "https://bank.demo.taler.net/"],
diff --git a/packages/demobank-ui/src/stories.test.ts b/packages/demobank-ui/src/stories.test.ts
index e68788f16..07db7d8cf 100644
--- a/packages/demobank-ui/src/stories.test.ts
+++ b/packages/demobank-ui/src/stories.test.ts
@@ -26,6 +26,7 @@ import * as pages from "./pages/index.stories.js";
import { ComponentChildren, VNode, h as create } from "preact";
import { BackendStateProviderTesting } from "./context/backend.js";
+import { AccessToken } from "./hooks/useCredentialsChecker.js";
setupI18n("en", { en: {} });
@@ -56,7 +57,7 @@ function DefaultTestingContext({
state: {
status: "loggedIn",
username: "test",
- password: "pwd",
+ token: "pwd" as AccessToken,
isUserAdministrator: false,
},
});
diff --git a/packages/demobank-ui/src/stories.tsx b/packages/demobank-ui/src/stories.tsx
index c6e8eb9ba..87848cb09 100644
--- a/packages/demobank-ui/src/stories.tsx
+++ b/packages/demobank-ui/src/stories.tsx
@@ -25,8 +25,6 @@ import * as components from "./components/index.examples.js";
import { renderStories } from "@gnu-taler/web-util/browser";
-import "./scss/main.scss";
-
function main(): void {
renderStories(
{ pages, components },
diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts
index 4ce0f140e..e7673f078 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -16,11 +16,12 @@
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
import {
+ ErrorNotification,
ErrorType,
HttpError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { ErrorMessage } from "./hooks/notification.js";
+
/**
* Validate (the number part of) an amount. If needed,
@@ -87,28 +88,6 @@ export enum CashoutStatus {
PENDING = "pending",
}
-// export function partialWithObjects<T extends object>(obj: T | undefined, () => complete): WithIntermediate<T> {
-// const root = obj === undefined ? {} : obj;
-// return Object.entries(root).([key, value]) => {
-
-// })
-// return undefined as any
-// }
-
-/**
- * Craft headers with Authorization and Content-Type.
- */
-// export function prepareHeaders(username?: string, password?: string): Headers {
-// const headers = new Headers();
-// if (username && password) {
-// headers.append(
-// "Authorization",
-// `Basic ${window.btoa(`${username}:${password}`)}`,
-// );
-// }
-// headers.append("Content-Type", "application/json");
-// return headers;
-// }
export const PAGE_SIZE = 20;
export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
@@ -120,11 +99,12 @@ export function buildRequestErrorMessage(
onClientError?: (status: HttpStatusCode) => TranslatedString | undefined;
onServerError?: (status: HttpStatusCode) => TranslatedString | undefined;
} = {},
-): ErrorMessage {
- let result: ErrorMessage;
+): ErrorNotification {
+ let result: ErrorNotification;
switch (cause.type) {
case ErrorType.TIMEOUT: {
result = {
+ type: "error",
title: i18n.str`Request timeout`,
};
break;
@@ -133,8 +113,9 @@ export function buildRequestErrorMessage(
const title =
specialCases.onClientError && specialCases.onClientError(cause.status);
result = {
+ type: "error",
title: title ? title : i18n.str`The server didn't accept the request`,
- description: cause?.payload?.error?.description,
+ description: cause?.payload?.error?.description as TranslatedString,
debug: JSON.stringify(cause),
};
break;
@@ -143,24 +124,27 @@ export function buildRequestErrorMessage(
const title =
specialCases.onServerError && specialCases.onServerError(cause.status);
result = {
+ type: "error",
title: title
? title
: i18n.str`The server had problems processing the request`,
- description: cause?.payload?.error?.description,
+ description: cause?.payload?.error?.description as TranslatedString,
debug: JSON.stringify(cause),
};
break;
}
case ErrorType.UNREADABLE: {
result = {
+ type: "error",
title: i18n.str`Unexpected error`,
- description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}`,
+ description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}` as TranslatedString,
debug: JSON.stringify(cause),
};
break;
}
case ErrorType.UNEXPECTED: {
result = {
+ type: "error",
title: i18n.str`Unexpected error`,
debug: JSON.stringify(cause),
};