From 39c3e36f86eee6297054c9b18480b41f4c026dca Mon Sep 17 00:00:00 2001 From: mtyszczak <mateusz.tyszczak@gmail.com> Date: Mon, 17 Mar 2025 12:07:38 +0100 Subject: [PATCH] Add loading to button --- src/App.vue | 24 ++++++++++---- .../wallets/metamask/MetamaskConnect.vue | 3 +- src/components/sidebar/AppSidebar.vue | 5 ++- src/components/ui/button/Button.vue | 16 +++++++-- src/components/utilcards/AuthorityCard.vue | 3 +- .../utilcards/ConfirmAccountUpdateCard.vue | 6 ++-- .../utilcards/ConfirmCreateAccountCard.vue | 4 +-- src/components/utilcards/MemoEncryptCard.vue | 21 +++++++++--- .../utilcards/SignTransactionCard.vue | 11 ++++--- src/lib/parse-error.ts | 33 ------------------- src/stores/user.store.ts | 29 ++++++++++++++++ src/stores/wax.store.ts | 4 +-- src/utils/parse-error.ts | 24 ++++++++++++++ 13 files changed, 121 insertions(+), 62 deletions(-) delete mode 100644 src/lib/parse-error.ts create mode 100644 src/stores/user.store.ts create mode 100644 src/utils/parse-error.ts diff --git a/src/App.vue b/src/App.vue index 8e91d24..bdaa168 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,18 +6,16 @@ import AppSidebar from '@/components/sidebar'; import { SidebarProvider } from '@/components/ui/sidebar'; import ToggleSidebar from './components/sidebar/ToggleSidebar.vue'; import { Toaster } from 'vue-sonner'; +import { useUserStore } from './stores/user.store'; +import { getWax } from './stores/wax.store'; const WalletOnboarding = defineAsyncComponent(() => import('@/components/onboarding/index')); const hasUser = ref(true); const settingsStore = useSettingsStore(); const walletStore = useWalletStore(); -onMounted(() => { - settingsStore.loadSettings(); - hasUser.value = settingsStore.settings.account !== undefined; - if (hasUser.value) - void walletStore.createWalletFor(settingsStore.settings); - +const userStore = useUserStore(); +onMounted(async() => { if (window.matchMedia) { const media = window.matchMedia('(prefers-color-scheme: dark)'); if (media.matches) @@ -26,8 +24,17 @@ onMounted(() => { document.documentElement.classList[event.matches ? 'add' : 'remove']('dark'); }); } + + settingsStore.loadSettings(); + hasUser.value = settingsStore.settings.account !== undefined; + if (hasUser.value) { + void walletStore.createWalletFor(settingsStore.settings); + const wax = await getWax(); + const { accounts: [ account ] } = await wax.api.database_api.find_accounts({ accounts: [ settingsStore.settings.account! ], delayed_votes_active: false }); + void userStore.setUserData(account); + } }); -const complete = (data: { account: string; wallet: UsedWallet }) => { +const complete = async(data: { account: string; wallet: UsedWallet }) => { hasUser.value = true; const settings = { account: data.account, @@ -35,6 +42,9 @@ const complete = (data: { account: string; wallet: UsedWallet }) => { }; settingsStore.setSettings(settings); void walletStore.createWalletFor(settings); + const wax = await getWax(); + const { accounts: [ account ] } = await wax.api.database_api.find_accounts({ accounts: [ settingsStore.settings.account! ], delayed_votes_active: false }); + void userStore.setUserData(account); }; </script> diff --git a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue index b5c6d37..cc2039b 100644 --- a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue +++ b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue @@ -15,7 +15,7 @@ import { Separator } from '@/components/ui/separator'; import PublicKey from '@/components/hive/PublicKey.vue'; import { Checkbox } from '@/components/ui/checkbox' import { getWax } from '@/stores/wax.store'; -import { AccountAuthorityUpdateOperation, type TRole } from "@hiveio/wax/vite"; +import type { TRole } from "@hiveio/wax/vite"; const emit = defineEmits(["setaccount", "close"]); @@ -135,6 +135,7 @@ const generateAccountUpdateTransaction = async(): Promise<string> => { const wax = await getWax(); const tx = await wax.createTransaction(); const accountName = updateAccountNameOperation.value!.startsWith('@') ? updateAccountNameOperation.value!.slice(1) : updateAccountNameOperation.value!; + const { AccountAuthorityUpdateOperation } = await import("@hiveio/wax/vite"); const op = await AccountAuthorityUpdateOperation.createFor(wax, accountName); for(const key in updateAuthType) { if (updateAuthType[key as TRole]) diff --git a/src/components/sidebar/AppSidebar.vue b/src/components/sidebar/AppSidebar.vue index 7fb651a..4e69153 100644 --- a/src/components/sidebar/AppSidebar.vue +++ b/src/components/sidebar/AppSidebar.vue @@ -6,6 +6,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { useSettingsStore, getWalletIcon } from '@/stores/settings.store'; import { computed } from 'vue'; import { Button } from '@/components/ui/button'; +import { useUserStore } from "@/stores/user.store"; const settingsStore = useSettingsStore(); const hasUser = computed(() => settingsStore.settings.account !== undefined); @@ -17,6 +18,8 @@ const logout = () => { const { toggleSidebar, isMobile } = useSidebar(); +const userStore = useUserStore(); + const items = [ { title: "Home", @@ -51,7 +54,7 @@ const items = [ <SidebarHeader class="pb-0"> <div class="flex items-center rounded-lg p-2 mt-1 mx-1 bg-background/40 border" v-if="settingsStore.isLoaded && hasUser"> <Avatar class="w-8 h-8 mr-2"> - <AvatarImage src="https://github.com/unovue.png" alt="@unovue" /> + <AvatarImage v-if="userStore.profileImage" :src="userStore.profileImage" /> <AvatarFallback>{{ settingsStore.settings.account?.slice(0, 2) }}</AvatarFallback> </Avatar> <span class="font-bold">@{{ settingsStore.settings.account }}</span> diff --git a/src/components/ui/button/Button.vue b/src/components/ui/button/Button.vue index 17dc84d..3ba549b 100644 --- a/src/components/ui/button/Button.vue +++ b/src/components/ui/button/Button.vue @@ -3,15 +3,19 @@ import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' import { Primitive, type PrimitiveProps } from 'reka-ui' import { type ButtonVariants, buttonVariants } from '.' +import { mdiLoading } from '@mdi/js' interface Props extends PrimitiveProps { variant?: ButtonVariants['variant'] size?: ButtonVariants['size'] class?: HTMLAttributes['class'] + loading?: boolean + disabled?: boolean } const props = withDefaults(defineProps<Props>(), { as: 'button', + loading: false }) </script> @@ -19,8 +23,16 @@ const props = withDefaults(defineProps<Props>(), { <Primitive :as="as" :as-child="asChild" - :class="cn(buttonVariants({ variant, size }), props.class)" + :disabled="loading || disabled" + :class="[ cn(buttonVariants({ variant, size }), props.class)]" > - <slot /> + <span v-if="loading" class="animate-spin absolute mx-auto"> + <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path :style="{ + fill: !variant || variant === 'default' ? 'hsl(var(--background))' : 'hsl(var(--foreground))' + }" :d="mdiLoading"/></svg> + </span> + <span :style="{ 'visibility': loading ? 'hidden' : 'visible' }" class="inline-flex items-center justify-center gap-2"> + <slot/> + </span> </Primitive> </template> diff --git a/src/components/utilcards/AuthorityCard.vue b/src/components/utilcards/AuthorityCard.vue index c7d6f1b..d387af1 100644 --- a/src/components/utilcards/AuthorityCard.vue +++ b/src/components/utilcards/AuthorityCard.vue @@ -4,7 +4,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { mdiAccountKeyOutline } from '@mdi/js'; import { useSettingsStore } from '@/stores/settings.store'; import { computed, onMounted, ref, watch } from 'vue'; -import { AccountAuthorityUpdateOperation, type authority } from '@hiveio/wax/vite'; +import type { authority } from '@hiveio/wax/vite'; import { getWax } from '@/stores/wax.store'; import PublicKey from '@/components/hive/PublicKey.vue'; @@ -20,6 +20,7 @@ const retrieveAuthority = async() => { try { const wax = await getWax(); + const { AccountAuthorityUpdateOperation } = await import("@hiveio/wax/vite"); const op = await AccountAuthorityUpdateOperation.createFor(wax, settingsStore.settings.account!); memoKey.value = op.role("memo").value; postingAuthority.value = op.role("posting").value; diff --git a/src/components/utilcards/ConfirmAccountUpdateCard.vue b/src/components/utilcards/ConfirmAccountUpdateCard.vue index 9eb9573..22b9094 100644 --- a/src/components/utilcards/ConfirmAccountUpdateCard.vue +++ b/src/components/utilcards/ConfirmAccountUpdateCard.vue @@ -9,8 +9,7 @@ import { onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; import { getWax } from '@/stores/wax.store'; import { useWalletStore } from '@/stores/wallet.store'; -import { AccountAuthorityUpdateOperation } from '@hiveio/wax'; -import { toastError } from '@/lib/parse-error'; +import { toastError } from '@/utils/parse-error'; const settings = useSettingsStore(); @@ -43,6 +42,7 @@ const updateAuthority = async() => { const wax = await getWax(); const tx = await wax.createTransaction(); + const { AccountAuthorityUpdateOperation } = await import("@hiveio/wax/vite"); const op = await AccountAuthorityUpdateOperation.createFor(wax, creator.value.startsWith('@') ? creator.value.slice(1) : creator.value); if (memoKey.value) op.role("memo").set(memoKey.value); @@ -95,7 +95,7 @@ const updateAuthority = async() => { <Label for="updateAuthority_ownerKey">Add Owner Key</Label> <Input id="updateAuthority_ownerKey" placeholder="Nothing to add" v-model="ownerKey" class="my-2" /> </div> - <Button class="my-2" @click="updateAuthority" :disabled="isLoading">Update Authority</Button> + <Button class="my-2" @click="updateAuthority" :loading="isLoading">Update Authority</Button> <p>Note: By clicking the above button, the transaction will be created, signed, and broadcasted immediately to the mainnet chain</p> </div> </CardContent> diff --git a/src/components/utilcards/ConfirmCreateAccountCard.vue b/src/components/utilcards/ConfirmCreateAccountCard.vue index ba0859f..947bcdd 100644 --- a/src/components/utilcards/ConfirmCreateAccountCard.vue +++ b/src/components/utilcards/ConfirmCreateAccountCard.vue @@ -12,7 +12,7 @@ import { onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; import { getWax } from '@/stores/wax.store'; import { useWalletStore } from '@/stores/wallet.store'; -import { toastError } from '@/lib/parse-error'; +import { toastError } from '@/utils/parse-error'; const settings = useSettingsStore(); @@ -178,7 +178,7 @@ const createAccount = async() => { <Label for="createAccount_r3">Create claimed</Label> </div> </RadioGroup> - <Button @click="createAccount" :disabled="isLoading">Create account</Button> + <Button @click="createAccount" :loading="isLoading">Create account</Button> <p>Note: By clicking the above button, the transaction will be created, signed, and broadcasted immediately to the mainnet chain</p> </div> </CardContent> diff --git a/src/components/utilcards/MemoEncryptCard.vue b/src/components/utilcards/MemoEncryptCard.vue index 5e7912d..c429ee3 100644 --- a/src/components/utilcards/MemoEncryptCard.vue +++ b/src/components/utilcards/MemoEncryptCard.vue @@ -9,7 +9,7 @@ import { Switch } from '@/components/ui/switch'; import { useWalletStore } from '@/stores/wallet.store'; import { getWax } from '@/stores/wax.store'; import { useSettingsStore } from '@/stores/settings.store'; -import { toastError } from '@/lib/parse-error'; +import { toastError } from '@/utils/parse-error'; const walletStore = useWalletStore(); const settingsStore = useSettingsStore(); @@ -18,6 +18,7 @@ const hasWallet = computed(() => walletStore.hasWallet); const wallet = computed(() => walletStore.wallet); const isEncrypt = ref(false); +const isLoading = ref(false); const encryptForKey = ref(''); const inputData = ref(''); const outputData = ref(''); @@ -37,13 +38,21 @@ const getMemoKeyForUser = async(user: string): Promise<string | void> => { } const useMyMemoKey = async () => { - const key = await getMemoKeyForUser(settingsStore.account!); - if (key) - encryptForKey.value = key; + try { + isLoading.value = true; + + const key = await getMemoKeyForUser(settingsStore.account!); + if (key) + encryptForKey.value = key; + } finally { + isLoading.value = false; + } } const encryptOrDecrypt = async () => { try { + isLoading.value = true; + if (isEncrypt.value) { let publicKey: string; let accountOrKey = encryptForKey.value; @@ -60,6 +69,8 @@ const encryptOrDecrypt = async () => { } } catch (error) { toastError(`Error ${isEncrypt.value ? 'encrypting' : 'decrypting'} memo`, error); + } finally { + isLoading.value = false; } }; </script> @@ -84,7 +95,7 @@ const encryptOrDecrypt = async () => { <div class="flex mb-4 underline text-sm" v-if="isEncrypt"> <a @click="useMyMemoKey" class="ml-auto mr-1 cursor-pointer" style="color: hsla(var(--foreground) / 70%)">Use my memo key</a> </div> - <Button :disabled="!hasWallet || (!encryptForKey && isEncrypt)" @click="encryptOrDecrypt">{{ isEncrypt ? "Encrypt" : "Decrypt" }}</Button> + <Button :loading="isLoading" :disabled="!hasWallet || (!encryptForKey && isEncrypt)" @click="encryptOrDecrypt">{{ isEncrypt ? "Encrypt" : "Decrypt" }}</Button> <Textarea v-model="outputData" placeholder="Output" copy-enabled class="my-4" disabled/> </CardContent> </Card> diff --git a/src/components/utilcards/SignTransactionCard.vue b/src/components/utilcards/SignTransactionCard.vue index d96fa68..fe94e75 100644 --- a/src/components/utilcards/SignTransactionCard.vue +++ b/src/components/utilcards/SignTransactionCard.vue @@ -8,7 +8,7 @@ import { useWalletStore } from '@/stores/wallet.store'; import { getWax } from '@/stores/wax.store'; import type { ITransactionBase, TRole } from '@hiveio/wax/vite'; import { useRouter } from 'vue-router'; -import { toastError } from '@/lib/parse-error'; +import { toastError } from '@/utils/parse-error'; const walletStore = useWalletStore(); @@ -21,6 +21,7 @@ const outputData = ref(''); const router = useRouter(); const isLoading = ref(false); +const isBroadcasting = ref(false); const sign = async () => { try { @@ -63,7 +64,7 @@ const sign = async () => { const broadcast = async () => { try { - isLoading.value = true; + isBroadcasting.value = true; const wax = await getWax(); @@ -74,7 +75,7 @@ const broadcast = async () => { } catch (error) { toastError('Error broadcasting transaction', error); } finally { - isLoading.value = false; + isBroadcasting.value = false; } }; @@ -95,11 +96,11 @@ onMounted(() => { <CardContent> <Textarea v-model="inputData" placeholder="Transaction in API JSON form" class="my-4"/> <div class="my-4 space-x-4"> - <Button :disabled="!inputData || !hasWallet || isLoading" @click="sign">Sign transaction</Button> + <Button :disabled="!inputData || !hasWallet || isBroadcasting" :loading="isLoading" @click="sign">Sign transaction</Button> </div> <Textarea v-model="outputData" placeholder="Signature" copy-enabled class="my-4" disabled/> <div class="my-4 space-x-4"> - <Button :disabled="!outputData || isLoading" @click="broadcast">Broadcast signed transaction</Button> + <Button :disabled="!outputData || isLoading" :loading="isBroadcasting" @click="broadcast">Broadcast signed transaction</Button> </div> </CardContent> </Card> diff --git a/src/lib/parse-error.ts b/src/lib/parse-error.ts deleted file mode 100644 index dd277c7..0000000 --- a/src/lib/parse-error.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { WaxChainApiError, WaxError } from "@hiveio/wax/vite"; -import { toast } from "vue-sonner"; - -export const toastError = (title: string, error: unknown) => { - let description: string; - - if (typeof error === "object" && error) { - if (error instanceof WaxError) { - if (error instanceof WaxChainApiError) { - if (error.apiError - && typeof error.apiError === "object" - && "error" in error.apiError - && typeof error.apiError.error === "object" - && error.apiError.error - && "message" in error.apiError.error - && typeof error.apiError.error.message === "string") - description = error.apiError.error.message; - else - description = error.message; - } else { - description = error.message; - } - } else if ("message" in error) { - description = (error as Error).message; - } else { - description = String(error); - } - } else { - description = String(error); - } - - toast.error(title, { description }); -}; diff --git a/src/stores/user.store.ts b/src/stores/user.store.ts new file mode 100644 index 0000000..306afb2 --- /dev/null +++ b/src/stores/user.store.ts @@ -0,0 +1,29 @@ +import { defineStore } from "pinia" +import type { ApiAccount } from "@hiveio/wax/vite"; + +export const useUserStore = defineStore('user', { + state: () => ({ + isReady: false, + parsedJsonMetadata: undefined as undefined | Record<string, any>, + userData: undefined as undefined | ApiAccount + }), + getters: { + profileImage: (ctx): undefined | string => ctx.isReady ? ctx.parsedJsonMetadata?.profile?.profile_image : undefined, + name: (ctx): undefined | string => ctx.isReady ? ctx.parsedJsonMetadata?.profile?.name : undefined, + about: (ctx): undefined | string => ctx.isReady ? ctx.parsedJsonMetadata?.profile?.about : undefined, + website: (ctx): undefined | string => ctx.isReady ? ctx.parsedJsonMetadata?.profile?.website : undefined + }, + actions: { + // Used for logout + resetSettings() { + this.isReady = false; + this.parsedJsonMetadata = undefined; + this.userData = undefined; + }, + setUserData(data: ApiAccount) { + this.userData = data; + this.parsedJsonMetadata = JSON.parse(data.posting_json_metadata); + this.isReady = true; + } + } +}) diff --git a/src/stores/wax.store.ts b/src/stores/wax.store.ts index 0ebd22b..ff6d9f5 100644 --- a/src/stores/wax.store.ts +++ b/src/stores/wax.store.ts @@ -1,4 +1,4 @@ -import { createHiveChain, type TWaxExtended, type asset } from "@hiveio/wax/vite"; +import type { TWaxExtended, asset } from "@hiveio/wax/vite"; export interface WaxApi { database_api: { @@ -17,7 +17,7 @@ let chain: TWaxExtended<WaxApi>; export const getWax = async() => { if (!chain) - chain = (await createHiveChain()).extend<WaxApi>(); + chain = (await (await import("@hiveio/wax/vite")).createHiveChain()).extend<WaxApi>(); return chain; }; diff --git a/src/utils/parse-error.ts b/src/utils/parse-error.ts new file mode 100644 index 0000000..18b2ef1 --- /dev/null +++ b/src/utils/parse-error.ts @@ -0,0 +1,24 @@ +import { toast } from "vue-sonner"; + +export const toastError = (title: string, error: unknown) => { + let description: string; + + if (typeof error === "object" && error && "message" in error) { + if ("name" in error + && error.name === "WaxChainApiError" + && "apiError" in error + && error.apiError + && typeof error.apiError === "object" + && "error" in error.apiError + && typeof error.apiError.error === "object" + && error.apiError.error + && "message" in error.apiError.error + && typeof error.apiError.error.message === "string") + description = error.apiError.error.message as string; + else + description = error.message as string; + } else + description = String(error); + + toast.error(title, { description }); +}; -- GitLab