aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/components
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-10-21 20:25:38 -0300
committerSebastian <sebasjm@gmail.com>2023-10-21 20:25:50 -0300
commit2ac73949e7cb8de44e56f2fecae617efab15671e (patch)
tree144a97d71bc9fa964675ef0cc764087ceb14e8eb /packages/demobank-ui/src/components
parent4b98b693d696d90f30f0a6546b0e1f4bc181a5f2 (diff)
downloadwallet-core-2ac73949e7cb8de44e56f2fecae617efab15671e.tar.xz
more ui
Diffstat (limited to 'packages/demobank-ui/src/components')
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx159
-rw-r--r--packages/demobank-ui/src/components/CopyButton.tsx32
-rw-r--r--packages/demobank-ui/src/components/LangSelector.tsx3
-rw-r--r--packages/demobank-ui/src/components/Routing.tsx208
-rw-r--r--packages/demobank-ui/src/components/Transactions/views.tsx12
-rw-r--r--packages/demobank-ui/src/components/app.tsx3
6 files changed, 323 insertions, 94 deletions
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx
index 0602f507e..32fe0aa9e 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -14,7 +14,7 @@
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";
@@ -33,55 +33,118 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
const { i18n } = useTranslationContext();
- if (!cashouts.length) {
- return (
- <div>
- <i18n.Translate>No cashout at the moment</i18n.Translate>
- </div>
- );
- }
+ if (!cashouts.length) return <div />
+ const txByDate = cashouts.reduce((prev, cur) => {
+ const d = cur.creation_time.t_s === "never"
+ ? ""
+ : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy")
+ if (!prev[d]) {
+ prev[d] = []
+ }
+ prev[d].push(cur)
+ return prev
+ }, {} as Record<string, typeof cashouts>)
return (
- <div class="results">
- <table class="pure-table pure-table-striped">
- <thead>
- <tr>
- <th>{i18n.str`Created`}</th>
- <th>{i18n.str`Confirmed`}</th>
- <th>{i18n.str`Total debit`}</th>
- <th>{i18n.str`Total credit`}</th>
- <th>{i18n.str`Status`}</th>
- <th>{i18n.str`Subject`}</th>
- </tr>
- </thead>
- <tbody>
- {cashouts.map((item, idx) => {
- return (
- <tr key={idx}>
- <td>{item.creation_time.t_s === "never" ? i18n.str`never` : format(item.creation_time.t_s, "dd/MM/yyyy HH:mm:ss")}</td>
- <td>
- {item.confirmation_time
+ <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 cashouts</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`Created`}</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`Confirmed`}</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`Total debit`}</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`Total credit`}</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`Status`}</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`Subject`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Object.entries(txByDate).map(([date, txs], idx) => {
+ return <Fragment key={idx}>
+ <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 creationTime = item.creation_time.t_s === "never" ? "" : format(item.creation_time.t_s * 1000, "HH:mm:ss")
+ const confirmationTime = item.confirmation_time
? item.confirmation_time.t_s === "never" ? i18n.str`never` : format(item.confirmation_time.t_s, "dd/MM/yyyy HH:mm:ss")
- : "-"}
- </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
- href="#"
- onClick={(e) => {
- e.preventDefault();
- onSelected(item.id);
- }}
- >
- {item.subject}
- </a>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
+ : "-"
+ return (<tr key={idx} class="border-b border-gray-200 last:border-none">
+
+ <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <div class="font-medium text-gray-900">{creationTime}</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 ? (
+ <span data-negative={item.negative ? "true" : "false"} class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
+ <RenderAmount value={item.amount} />
+ </span>
+ ) : (
+ <span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
+ )}</dd>
+
+ <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt>
+ <dd class="mt-1 truncate text-gray-500 sm:hidden">
+ {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart}
+ </dd>
+ <dd class="mt-1 text-gray-500 sm:hidden" >
+ <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
+ {item.subject}
+ </pre>
+ </dd>
+ </dl> */}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{confirmationTime}</td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} /></td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} /></td>
+
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.status}</td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
+ <a href="#" onClick={(e) => {
+ e.preventDefault();
+ onSelected(item.id);
+ }}>
+ {item.subject}
+ </a>
+ </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/CopyButton.tsx b/packages/demobank-ui/src/components/CopyButton.tsx
index b36de770e..ca1ceaa8a 100644
--- a/packages/demobank-ui/src/components/CopyButton.tsx
+++ b/packages/demobank-ui/src/components/CopyButton.tsx
@@ -5,31 +5,21 @@ 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 xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
</svg>
)
};
export function CopiedIcon(): VNode {
return (
- <svg 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 xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
)
};
-export function CopyButton({ getContent }: { getContent: () => string }): VNode {
+export function CopyButton({ class: clazz, getContent }: { class: string, getContent: () => string }): VNode {
const [copied, setCopied] = useState(false);
function copyText(): void {
navigator.clipboard.writeText(getContent() || "");
@@ -45,16 +35,14 @@ export function CopyButton({ getContent }: { getContent: () => string }): VNode
if (!copied) {
return (
- <button class="text-white" onClick={copyText} style={{ width: 16, height: 16, fontSize: "initial" }}>
+ <button class={clazz} onClick={copyText} >
<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>
+ <button class={clazz} disabled>
+ <CopiedIcon />
+ </button>
);
-} \ No newline at end of file
+}
diff --git a/packages/demobank-ui/src/components/LangSelector.tsx b/packages/demobank-ui/src/components/LangSelector.tsx
index c1d0f64ef..7cf0300df 100644
--- a/packages/demobank-ui/src/components/LangSelector.tsx
+++ b/packages/demobank-ui/src/components/LangSelector.tsx
@@ -23,6 +23,7 @@ import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { strings as messages } from "../i18n/strings.js";
+import langIcon from "../assets/lang.svg";
type LangsNames = {
[P in keyof typeof messages]: string;
@@ -69,7 +70,7 @@ export function LangSelector(): VNode {
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" />
+ <img src={langIcon} 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">
diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx
index 04cf96190..1d587fe32 100644
--- a/packages/demobank-ui/src/components/Routing.tsx
+++ b/packages/demobank-ui/src/components/Routing.tsx
@@ -19,19 +19,26 @@ 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 { useBackendState } from "../hooks/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 { AdminHome } from "../pages/admin/AdminHome.js";
+import { CreateCashout } from "../pages/business/CreateCashout.js";
import { bankUiSettings } from "../settings.js";
+import { ShowAccountDetails } from "../pages/ShowAccountDetails.js";
+import { UpdateAccountPassword } from "../pages/UpdateAccountPassword.js";
+import { RemoveAccount } from "../pages/admin/RemoveAccount.js";
+import { CreateNewAccount } from "../pages/admin/CreateNewAccount.js";
+import { CashoutListForAccount } from "../pages/admin/CashoutListForAccount.js";
+import { ShowCashoutDetails } from "../pages/business/ShowCashoutDetails.js";
+import { WireTransfer } from "../pages/admin/Account.js";
export function Routing(): VNode {
const history = createHashHistory();
- const backend = useBackendContext();
+ const backend = useBackendState();
const { i18n } = useTranslationContext();
if (backend.state.status === "loggedOut") {
@@ -90,7 +97,7 @@ export function Routing(): VNode {
const { isUserAdministrator, username } = backend.state
return (
- <BankFrame account={backend.state.username}>
+ <BankFrame account={username}>
<Router history={history}>
<Route
path="/operation/:wopid"
@@ -107,6 +114,167 @@ export function Routing(): VNode {
path="/public-accounts"
component={() => <PublicHistoriesPage />}
/>
+
+ <Route
+ path="/new-account"
+ component={() => <CreateNewAccount
+ onCancel={() => {
+ route("/account")
+ }}
+ onCreateSuccess={() => {
+ route("/account")
+ }}
+ />}
+ />
+
+ <Route
+ path="/profile/:account/details"
+ component={({ account }: { account: string }) => (
+ <ShowAccountDetails
+ account={account}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onClear={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/profile/:account/change-password"
+ component={({ account }: { account: string }) => (
+ <UpdateAccountPassword
+ focus
+ account={account}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onCancel={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+ <Route
+ path="/profile/:account/delete"
+ component={({ account }: { account: string }) => (
+ <RemoveAccount
+ account={account}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onCancel={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/profile/:account/cashouts"
+ component={({ account }: { account: string }) => (
+ <CashoutListForAccount
+ account={account}
+ onSelected={(cid) => {
+ route(`/cashout/${cid}`)
+ }}
+ onClose={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/my-profile"
+ component={() => (
+ <ShowAccountDetails
+ account={username}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onClear={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+ <Route
+ path="/my-password"
+ component={() => (
+ <UpdateAccountPassword
+ focus
+ account={username}
+ onUpdateSuccess={() => {
+ route("/account")
+ }}
+ onCancel={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/my-cashouts"
+ component={() => (
+ <CashoutListForAccount
+ account={username}
+ onSelected={(cid) => {
+ route(`/cashout/${cid}`)
+ }}
+ onClose={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/new-cashout"
+ component={() => (
+ <CreateCashout
+ account={username}
+ onComplete={(cid) => {
+ route(`/cashout/${cid}`);
+ }}
+ onCancel={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+
+ <Route
+ path="/cashout/:cid"
+ component={({ cid }: { cid: string }) => (
+ <ShowCashoutDetails
+ id={cid}
+ onCancel={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+
+
+ <Route
+ path="/wire-transfer/:dest"
+ component={({ dest }: { dest: string }) => (
+ <WireTransfer
+ toAccount={dest}
+ onCancel={() => {
+ route("/account")
+ }}
+ onSuccess={() => {
+ route("/account")
+ }}
+ />
+ )}
+ />
+
<Route
path="/account"
component={() => {
@@ -115,6 +283,22 @@ export function Routing(): VNode {
onRegister={() => {
route("/register");
}}
+ onCreateAccount={() => {
+ route("/new-account")
+ }}
+ onShowAccountDetails={(aid) => {
+ route(`/profile/${aid}/details`)
+ }}
+ onRemoveAccount={(aid) => {
+ route(`/profile/${aid}/delete`)
+ }}
+ onShowCashoutForAccount={(aid) => {
+ route(`/profile/${aid}/cashouts`)
+ }}
+ onUpdateAccountPassword={(aid) => {
+ route(`/profile/${aid}/change-password`)
+
+ }}
/>;
} else {
return <HomePage
@@ -132,20 +316,6 @@ export function Routing(): VNode {
}
}}
/>
- <Route
- path="/business"
- component={() => (
- <BusinessAccount
- account={username}
- onClose={() => {
- route("/account");
- }}
- onRegister={() => {
- route("/register");
- }}
- />
- )}
- />
<Route default component={Redirect} to="/account" />
</Router>
</BankFrame>
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx
index 5cdb47a0c..47daf8963 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Transactions/views.tsx
@@ -86,11 +86,13 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
<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}
+ {item.negative ? i18n.str`to` : i18n.str`from`} <a href={`#/wire-transfer/${item.counterpart}`} class="text-indigo-600 hover:text-indigo-900">
+ {item.counterpart}
+ </a>
</dd>
<dd class="mt-1 text-gray-500 sm:hidden" >
<pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
- {item.subject}
+ {item.subject}
</pre>
</dd>
</dl>
@@ -102,7 +104,11 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode
<span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
)}
</td>
- <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">
+ <a href={`#/wire-transfer/${item.counterpart}`} class="text-indigo-600 hover:text-indigo-900">
+ {item.counterpart}
+ </a>
+ </td>
<td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td>
</tr>)
})}
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx
index beb24da57..55e1178fe 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -27,6 +27,7 @@ import { BankCoreApiProvider } from "../context/config.js";
import { strings } from "../i18n/strings.js";
import { bankUiSettings } from "../settings.js";
import { Routing } from "./Routing.js";
+import { BankFrame } from "../pages/BankFrame.js";
const WITH_LOCAL_STORAGE_CACHE = false;
const App: FunctionalComponent = () => {
@@ -34,7 +35,7 @@ const App: FunctionalComponent = () => {
return (
<TranslationProvider source={strings}>
<BackendStateProvider>
- <BankCoreApiProvider baseUrl={baseUrl}>
+ <BankCoreApiProvider baseUrl={baseUrl} frameOnError={BankFrame}>
<SWRConfig
value={{
provider: WITH_LOCAL_STORAGE_CACHE