diff options
author | Özgür Kesim <oec-taler@kesim.org> | 2023-10-06 16:33:05 +0200 |
---|---|---|
committer | Özgür Kesim <oec-taler@kesim.org> | 2023-10-06 16:33:05 +0200 |
commit | fe7b51ef2736edbf04f5bbd9d19f2a2d04baccc2 (patch) | |
tree | 66c68c8d6a666f6e74dc663c9ee4f07879f6626c /packages/demobank-ui/src | |
parent | 35611f0bf9cf67638b171c2a300fab1797d3d8f0 (diff) | |
parent | 97d7be7503168f4f3bbd05905d32aa76ca1636b2 (diff) |
Merge branch 'master' into age-withdraw
Diffstat (limited to 'packages/demobank-ui/src')
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" }}><invalid value></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" }}><{i18n.str`invalid value`}></span> + )}</dd> + + <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt> + <dd class="mt-1 truncate text-gray-500 sm:hidden"> + {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart} + </dd> + </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" }}><{i18n.str`invalid value`}></span> + )} + </td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td> + <td class="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 "{data.paytoUri}"</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> - - <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 "{error.data.payto_uri}"</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> - - <span class="currency">{`${balance.currency}`}</span> - </span> - )} - </td> - <td> - <a - href="#" - onClick={(e) => { - e.preventDefault(); - setUpdatePassword(item.username); - }} - > - change password - </a> - - <a - href="#" - onClick={(e) => { - e.preventDefault(); - setShowCashouts(item.username); - }} - > - cashouts - </a> - - <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> - - <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 © 2014—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">></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 © 2014—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 "{payto}"</div> + ); +} +export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) { + return ( + <div>Withdrawal uri from server is not valid "{uri}"</div> + ); +} +export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) { + return ( + <div>Reserve from server is not valid "{reserve}"</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`} + <em> + {captchaNumbers.a} + {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">💵</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">↔</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> - <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> - <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> - <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> - <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> - - <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`} - <em> - {captchaNumbers.a} + {captchaNumbers.b} - </em> - ? - </label> - - <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`} + <em> + {captchaNumbers.a} + {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> - - <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 Binary files differdeleted file mode 100644 index 7665ee336..000000000 --- a/packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf +++ /dev/null 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 Binary files differdeleted file mode 100644 index ab6b25ded..000000000 --- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot +++ /dev/null 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 Binary files differdeleted file mode 100644 index 824be10fa..000000000 --- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf +++ /dev/null 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 Binary files differdeleted file mode 100644 index 7e087c1de..000000000 --- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff +++ /dev/null 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 Binary files differdeleted file mode 100644 index b5caa4ddc..000000000 --- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2 +++ /dev/null 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), }; |