From 98bcebd3ae9058b7d1037b1557f0b6201085d291 Mon Sep 17 00:00:00 2001
From: mtyszczak <mateusz.tyszczak@gmail.com>
Date: Wed, 19 Mar 2025 13:04:17 +0100
Subject: [PATCH] Minor fixes

---
 src/App.vue                                   |  2 +
 src/components/ErrorDialog.vue                | 45 ++++++++++++++
 src/components/navigation/AppHeader.vue       |  9 ++-
 src/components/ui/dialog/Dialog.vue           | 14 +++++
 src/components/ui/dialog/DialogClose.vue      | 11 ++++
 src/components/ui/dialog/DialogContent.vue    | 50 ++++++++++++++++
 .../ui/dialog/DialogDescription.vue           | 24 ++++++++
 src/components/ui/dialog/DialogFooter.vue     | 19 ++++++
 src/components/ui/dialog/DialogHeader.vue     | 16 +++++
 .../ui/dialog/DialogScrollContent.vue         | 59 +++++++++++++++++++
 src/components/ui/dialog/DialogTitle.vue      | 29 +++++++++
 src/components/ui/dialog/DialogTrigger.vue    | 11 ++++
 src/components/ui/dialog/index.ts             |  9 +++
 .../utilcards/ConfirmAccountUpdateCard.vue    | 10 ++--
 .../utilcards/ConfirmCreateAccountCard.vue    | 16 ++---
 src/components/utilcards/MemoEncryptCard.vue  |  4 +-
 .../utilcards/SignTransactionCard.vue         |  4 +-
 src/stores/error-dialog.store.ts              | 24 ++++++++
 src/utils/parse-error.ts                      | 14 ++++-
 19 files changed, 347 insertions(+), 23 deletions(-)
 create mode 100644 src/components/ErrorDialog.vue
 create mode 100644 src/components/ui/dialog/Dialog.vue
 create mode 100644 src/components/ui/dialog/DialogClose.vue
 create mode 100644 src/components/ui/dialog/DialogContent.vue
 create mode 100644 src/components/ui/dialog/DialogDescription.vue
 create mode 100644 src/components/ui/dialog/DialogFooter.vue
 create mode 100644 src/components/ui/dialog/DialogHeader.vue
 create mode 100644 src/components/ui/dialog/DialogScrollContent.vue
 create mode 100644 src/components/ui/dialog/DialogTitle.vue
 create mode 100644 src/components/ui/dialog/DialogTrigger.vue
 create mode 100644 src/components/ui/dialog/index.ts
 create mode 100644 src/stores/error-dialog.store.ts

diff --git a/src/App.vue b/src/App.vue
index 6c11479..6bd5063 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -8,6 +8,7 @@ import { Toaster } from 'vue-sonner';
 import { useUserStore } from '@/stores/user.store';
 import { getWax } from '@/stores/wax.store';
 import AppHeader from '@/components/navigation/AppHeader.vue';
+import ErrorDialog from './components/ErrorDialog.vue';
 
 const WalletOnboarding = defineAsyncComponent(() => import('@/components/onboarding/index'));
 
@@ -56,6 +57,7 @@ const complete = async(data: { account: string; wallet: UsedWallet }) => {
         </aside>
       </SidebarProvider>
       <Toaster theme="dark" closeButton richColors />
+      <ErrorDialog />
     </div>
   </div>
 </template>
diff --git a/src/components/ErrorDialog.vue b/src/components/ErrorDialog.vue
new file mode 100644
index 0000000..10eda67
--- /dev/null
+++ b/src/components/ErrorDialog.vue
@@ -0,0 +1,45 @@
+<script setup lang="ts">
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import { Button } from "@/components/ui/button";
+import { useErrorDialogStore } from '@/stores/error-dialog.store';
+import { toRaw } from 'vue';
+
+const errorStore = useErrorDialogStore();
+
+const updateOpen = (value: boolean) => {
+  if (value === false)
+    errorStore.closeError();
+};
+
+const logOriginator = () => {
+  console.error(toRaw(errorStore.originator));
+};
+
+const createErrorText = () => `${errorStore.title} - ${errorStore.description}`;
+</script>
+
+<template>
+  <Dialog :open="errorStore.hasError" class="max-w-[90vw]" @update:open="updateOpen">
+    <DialogContent class="max-w-[95vw] sm:max-w-[600px] md:max-w-[700px] lg:max-w-[800px] grid-rows-[auto_minmax(0,1fr)_auto] p-0 max-h-[90dvh]">
+      <DialogHeader class="p-6 pb-0">
+        <DialogTitle>{{ errorStore.title }}</DialogTitle>
+        <DialogDescription>
+          Read the error message below to understand what went wrong.
+        </DialogDescription>
+      </DialogHeader>
+      <div class="py-4 overflow-y-auto px-6">
+        <code>
+          <pre class="break-all whitespace-pre-wrap">{{ errorStore.description }}</pre>
+        </code>
+      </div>
+      <DialogFooter class="p-6 pt-0">
+        <Button variant="secondary" @click="logOriginator">
+          Log error to console
+        </Button>
+        <Button :copy="createErrorText">
+          Copy error message
+        </Button>
+      </DialogFooter>
+    </DialogContent>
+  </Dialog>
+</template>
\ No newline at end of file
diff --git a/src/components/navigation/AppHeader.vue b/src/components/navigation/AppHeader.vue
index fcb24b2..1961a5c 100644
--- a/src/components/navigation/AppHeader.vue
+++ b/src/components/navigation/AppHeader.vue
@@ -11,15 +11,14 @@ import { mdiLogout } from '@mdi/js';
 
 const settingsStore = useSettingsStore();
 const hasUser = computed(() => settingsStore.settings.account !== undefined);
+const walletStore = useWalletStore();
+const userStore = useUserStore();
 
 const logout = () => {
   settingsStore.resetSettings();
-  window.location.reload();
+  walletStore.resetWallet();
+  userStore.resetSettings();
 };
-
-const walletStore = useWalletStore();
-
-const userStore = useUserStore();
 </script>
 
 <template>
diff --git a/src/components/ui/dialog/Dialog.vue b/src/components/ui/dialog/Dialog.vue
new file mode 100644
index 0000000..9fc9c7d
--- /dev/null
+++ b/src/components/ui/dialog/Dialog.vue
@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'reka-ui'
+
+const props = defineProps<DialogRootProps>()
+const emits = defineEmits<DialogRootEmits>()
+
+const forwarded = useForwardPropsEmits(props, emits)
+</script>
+
+<template>
+  <DialogRoot v-bind="forwarded">
+    <slot />
+  </DialogRoot>
+</template>
diff --git a/src/components/ui/dialog/DialogClose.vue b/src/components/ui/dialog/DialogClose.vue
new file mode 100644
index 0000000..ba036b5
--- /dev/null
+++ b/src/components/ui/dialog/DialogClose.vue
@@ -0,0 +1,11 @@
+<script setup lang="ts">
+import { DialogClose, type DialogCloseProps } from 'reka-ui'
+
+const props = defineProps<DialogCloseProps>()
+</script>
+
+<template>
+  <DialogClose v-bind="props">
+    <slot />
+  </DialogClose>
+</template>
diff --git a/src/components/ui/dialog/DialogContent.vue b/src/components/ui/dialog/DialogContent.vue
new file mode 100644
index 0000000..d84f271
--- /dev/null
+++ b/src/components/ui/dialog/DialogContent.vue
@@ -0,0 +1,50 @@
+<script setup lang="ts">
+import { cn } from '@/lib/utils'
+import { X } from 'lucide-vue-next'
+import {
+  DialogClose,
+  DialogContent,
+  type DialogContentEmits,
+  type DialogContentProps,
+  DialogOverlay,
+  DialogPortal,
+  useForwardPropsEmits,
+} from 'reka-ui'
+import { computed, type HTMLAttributes } from 'vue'
+
+const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
+const emits = defineEmits<DialogContentEmits>()
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props
+
+  return delegated
+})
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits)
+</script>
+
+<template>
+  <DialogPortal>
+    <DialogOverlay
+      class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
+    />
+    <DialogContent
+      v-bind="forwarded"
+      :class="
+        cn(
+          'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
+          props.class,
+        )"
+    >
+      <slot />
+
+      <DialogClose
+        class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
+      >
+        <X class="w-4 h-4" />
+        <span class="sr-only">Close</span>
+      </DialogClose>
+    </DialogContent>
+  </DialogPortal>
+</template>
diff --git a/src/components/ui/dialog/DialogDescription.vue b/src/components/ui/dialog/DialogDescription.vue
new file mode 100644
index 0000000..5afbab0
--- /dev/null
+++ b/src/components/ui/dialog/DialogDescription.vue
@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import { cn } from '@/lib/utils'
+import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'reka-ui'
+import { computed, type HTMLAttributes } from 'vue'
+
+const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props
+
+  return delegated
+})
+
+const forwardedProps = useForwardProps(delegatedProps)
+</script>
+
+<template>
+  <DialogDescription
+    v-bind="forwardedProps"
+    :class="cn('text-sm text-muted-foreground', props.class)"
+  >
+    <slot />
+  </DialogDescription>
+</template>
diff --git a/src/components/ui/dialog/DialogFooter.vue b/src/components/ui/dialog/DialogFooter.vue
new file mode 100644
index 0000000..ac2d0c1
--- /dev/null
+++ b/src/components/ui/dialog/DialogFooter.vue
@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from 'vue'
+import { cn } from '@/lib/utils'
+
+const props = defineProps<{ class?: HTMLAttributes['class'] }>()
+</script>
+
+<template>
+  <div
+    :class="
+      cn(
+        'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
+        props.class,
+      )
+    "
+  >
+    <slot />
+  </div>
+</template>
diff --git a/src/components/ui/dialog/DialogHeader.vue b/src/components/ui/dialog/DialogHeader.vue
new file mode 100644
index 0000000..b2c9085
--- /dev/null
+++ b/src/components/ui/dialog/DialogHeader.vue
@@ -0,0 +1,16 @@
+<script setup lang="ts">
+import type { HTMLAttributes } from 'vue'
+import { cn } from '@/lib/utils'
+
+const props = defineProps<{
+  class?: HTMLAttributes['class']
+}>()
+</script>
+
+<template>
+  <div
+    :class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)"
+  >
+    <slot />
+  </div>
+</template>
diff --git a/src/components/ui/dialog/DialogScrollContent.vue b/src/components/ui/dialog/DialogScrollContent.vue
new file mode 100644
index 0000000..ee6b5e2
--- /dev/null
+++ b/src/components/ui/dialog/DialogScrollContent.vue
@@ -0,0 +1,59 @@
+<script setup lang="ts">
+import { cn } from '@/lib/utils'
+import { X } from 'lucide-vue-next'
+import {
+  DialogClose,
+  DialogContent,
+  type DialogContentEmits,
+  type DialogContentProps,
+  DialogOverlay,
+  DialogPortal,
+  useForwardPropsEmits,
+} from 'reka-ui'
+import { computed, type HTMLAttributes } from 'vue'
+
+const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
+const emits = defineEmits<DialogContentEmits>()
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props
+
+  return delegated
+})
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits)
+</script>
+
+<template>
+  <DialogPortal>
+    <DialogOverlay
+      class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
+    >
+      <DialogContent
+        :class="
+          cn(
+            'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
+            props.class,
+          )
+        "
+        v-bind="forwarded"
+        @pointer-down-outside="(event) => {
+          const originalEvent = event.detail.originalEvent;
+          const target = originalEvent.target as HTMLElement;
+          if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
+            event.preventDefault();
+          }
+        }"
+      >
+        <slot />
+
+        <DialogClose
+          class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
+        >
+          <X class="w-4 h-4" />
+          <span class="sr-only">Close</span>
+        </DialogClose>
+      </DialogContent>
+    </DialogOverlay>
+  </DialogPortal>
+</template>
diff --git a/src/components/ui/dialog/DialogTitle.vue b/src/components/ui/dialog/DialogTitle.vue
new file mode 100644
index 0000000..30cbb36
--- /dev/null
+++ b/src/components/ui/dialog/DialogTitle.vue
@@ -0,0 +1,29 @@
+<script setup lang="ts">
+import { cn } from '@/lib/utils'
+import { DialogTitle, type DialogTitleProps, useForwardProps } from 'reka-ui'
+import { computed, type HTMLAttributes } from 'vue'
+
+const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
+
+const delegatedProps = computed(() => {
+  const { class: _, ...delegated } = props
+
+  return delegated
+})
+
+const forwardedProps = useForwardProps(delegatedProps)
+</script>
+
+<template>
+  <DialogTitle
+    v-bind="forwardedProps"
+    :class="
+      cn(
+        'text-lg font-semibold leading-none tracking-tight',
+        props.class,
+      )
+    "
+  >
+    <slot />
+  </DialogTitle>
+</template>
diff --git a/src/components/ui/dialog/DialogTrigger.vue b/src/components/ui/dialog/DialogTrigger.vue
new file mode 100644
index 0000000..2984f37
--- /dev/null
+++ b/src/components/ui/dialog/DialogTrigger.vue
@@ -0,0 +1,11 @@
+<script setup lang="ts">
+import { DialogTrigger, type DialogTriggerProps } from 'reka-ui'
+
+const props = defineProps<DialogTriggerProps>()
+</script>
+
+<template>
+  <DialogTrigger v-bind="props">
+    <slot />
+  </DialogTrigger>
+</template>
diff --git a/src/components/ui/dialog/index.ts b/src/components/ui/dialog/index.ts
new file mode 100644
index 0000000..ca8cfea
--- /dev/null
+++ b/src/components/ui/dialog/index.ts
@@ -0,0 +1,9 @@
+export { default as Dialog } from './Dialog.vue'
+export { default as DialogClose } from './DialogClose.vue'
+export { default as DialogContent } from './DialogContent.vue'
+export { default as DialogDescription } from './DialogDescription.vue'
+export { default as DialogFooter } from './DialogFooter.vue'
+export { default as DialogHeader } from './DialogHeader.vue'
+export { default as DialogScrollContent } from './DialogScrollContent.vue'
+export { default as DialogTitle } from './DialogTitle.vue'
+export { default as DialogTrigger } from './DialogTrigger.vue'
diff --git a/src/components/utilcards/ConfirmAccountUpdateCard.vue b/src/components/utilcards/ConfirmAccountUpdateCard.vue
index 33bf986..193e353 100644
--- a/src/components/utilcards/ConfirmAccountUpdateCard.vue
+++ b/src/components/utilcards/ConfirmAccountUpdateCard.vue
@@ -78,23 +78,23 @@ const updateAuthority = async() => {
     </CardHeader>
     <CardContent>
       <div class="my-4 space-y-2">
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="updateAuthority_creator">Account To Update</Label>
           <Input id="updateAuthority_creator" v-model="creator" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="updateAuthority_memoKey">New Memo Key</Label>
           <Input id="updateAuthority_memoKey" placeholder="Nothing to update" v-model="memoKey" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="updateAuthority_postingKey">Add Posting Key</Label>
           <Input id="updateAuthority_postingKey" placeholder="Nothing to add" v-model="postingKey" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="updateAuthority_activeKey">Add Active Key</Label>
           <Input id="updateAuthority_activeKey" placeholder="Nothing to add" v-model="activeKey" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="updateAuthority_ownerKey">Add Owner Key</Label>
           <Input id="updateAuthority_ownerKey" placeholder="Nothing to add" v-model="ownerKey" class="my-2" />
         </div>
diff --git a/src/components/utilcards/ConfirmCreateAccountCard.vue b/src/components/utilcards/ConfirmCreateAccountCard.vue
index f6e2913..bc31d83 100644
--- a/src/components/utilcards/ConfirmCreateAccountCard.vue
+++ b/src/components/utilcards/ConfirmCreateAccountCard.vue
@@ -130,31 +130,31 @@ const createAccount = async() => {
     </CardHeader>
     <CardContent>
       <div class="my-4 space-y-4">
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="createAccount_creator">Creator Account Name</Label>
           <Input id="createAccount_creator" v-model="creator" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="createAccount_accountName">New Account Name</Label>
           <Input id="createAccount_accountName" v-model="accountName" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="createAccount_memoKey">Memo Key</Label>
           <Input id="createAccount_memoKey" v-model="memoKey" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="createAccount_postingKey">Posting Key</Label>
           <Input id="createAccount_postingKey" v-model="postingKey" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="createAccount_activeKey">Active Key</Label>
           <Input id="createAccount_activeKey" v-model="activeKey" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="createAccount_ownerKey">Owner Key</Label>
           <Input id="createAccount_ownerKey" v-model="ownerKey" class="my-2" />
         </div>
-        <div class="grid w-full max-w-sm items-center">
+        <div class="grid w-full items-center">
           <Label for="createAccount_postingMetadata">Posting Metadata</Label>
           <Textarea id="createAccount_postingMetadata" v-model="postingMetadata" class="my-2" />
         </div>
@@ -164,7 +164,7 @@ const createAccount = async() => {
             <span class="text-sm">Enable Delegation</span>
           </label>
         </div>
-        <div class="grid w-full max-w-sm items-center" v-if="enableDelegation">
+        <div class="grid w-full items-center" v-if="enableDelegation">
           <Label for="createAccount_delegationAmount">Delegation amount</Label>
           <div class="flex items-center space-x-2">
             <Input id="createAccount_delegationAmount" type="number" v-model="delegationAmount" min="0" class="my-2" />
diff --git a/src/components/utilcards/MemoEncryptCard.vue b/src/components/utilcards/MemoEncryptCard.vue
index 2b7b6c8..d1a3955 100644
--- a/src/components/utilcards/MemoEncryptCard.vue
+++ b/src/components/utilcards/MemoEncryptCard.vue
@@ -101,13 +101,13 @@ const encryptOrDecrypt = async () => {
           </TabsTrigger>
         </TabsList>
       </Tabs>
-      <Textarea v-model="inputData" placeholder="Input" class="my-4"/>
+      <Textarea v-model="inputData" placeholder="Input" class="my-4" height="200px"/>
       <Input v-model="encryptForKey" v-if="isEncrypt" placeholder="Receiver account or public key" class="mt-4"/>
       <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 :loading="isLoading" :disabled="(!encryptForKey && isEncrypt)" @click="encryptOrDecrypt">{{ isEncrypt ? "Encrypt" : "Decrypt" }}</Button>
-      <Textarea v-model="outputData" placeholder="Output" copy-enabled class="my-4" disabled/>
+      <Textarea v-model="outputData" placeholder="Output" copy-enabled class="my-4" height="200px" disabled/>
     </CardContent>
   </Card>
 </template>
\ No newline at end of file
diff --git a/src/components/utilcards/SignTransactionCard.vue b/src/components/utilcards/SignTransactionCard.vue
index 638d864..e82af94 100644
--- a/src/components/utilcards/SignTransactionCard.vue
+++ b/src/components/utilcards/SignTransactionCard.vue
@@ -99,11 +99,11 @@ onMounted(() => {
       <CardDescription class="mr-8">Use this module to sign the provided transaction</CardDescription>
     </CardHeader>
     <CardContent>
-      <Textarea v-model="inputData" placeholder="Transaction in API JSON form" class="my-4"/>
+      <Textarea v-model="inputData" placeholder="Transaction in API JSON form" class="my-4" height="200px"/>
       <div class="my-4 space-x-4">
         <Button :disabled="!inputData || isBroadcasting" :loading="isLoading" @click="sign">Sign transaction</Button>
       </div>
-      <Textarea v-model="outputData" placeholder="Signed transaction" copy-enabled class="my-4" disabled/>
+      <Textarea v-model="outputData" placeholder="Signed transaction" copy-enabled class="my-4" height="200px" disabled/>
       <div class="my-4 space-x-4">
         <Button :disabled="!outputData || isLoading" :loading="isBroadcasting" @click="broadcast">Broadcast signed transaction</Button>
       </div>
diff --git a/src/stores/error-dialog.store.ts b/src/stores/error-dialog.store.ts
new file mode 100644
index 0000000..35831ab
--- /dev/null
+++ b/src/stores/error-dialog.store.ts
@@ -0,0 +1,24 @@
+import { defineStore } from "pinia"
+
+export const useErrorDialogStore = defineStore('errorDialog', {
+  state: () => ({
+    title: undefined as undefined | string,
+    originator: undefined as unknown,
+    description: undefined as undefined | string
+  }),
+  getters: {
+    hasError: ctx => ctx.title !== undefined
+  },
+  actions: {
+    closeError() {
+      this.title = undefined;
+      this.originator = undefined;
+      this.description = undefined;
+    },
+    setError(title: string, originator: unknown, description?: string) {
+      this.title = title;
+      this.originator = originator;
+      this.description = description;
+    }
+  }
+})
diff --git a/src/utils/parse-error.ts b/src/utils/parse-error.ts
index 18b2ef1..3bde273 100644
--- a/src/utils/parse-error.ts
+++ b/src/utils/parse-error.ts
@@ -1,4 +1,5 @@
 import { toast } from "vue-sonner";
+import { useErrorDialogStore } from '@/stores/error-dialog.store';
 
 export const toastError = (title: string, error: unknown) => {
   let description: string;
@@ -20,5 +21,16 @@ export const toastError = (title: string, error: unknown) => {
   } else
     description = String(error);
 
-  toast.error(title, { description });
+  toast.error(title, {
+    description: description.length > 100 ? description.slice(0, 100) + "..." : description,
+    duration: 8_000,
+    action: {
+      label: "Details",
+      onClick: () => {
+        const errorStore = useErrorDialogStore();
+
+        errorStore.setError(title, error, description);
+      }
+    }
+  });
 };
-- 
GitLab