diff --git a/src/App.vue b/src/App.vue
index c3670ec..ccc382a 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -3,6 +3,7 @@ import {computed} from 'vue';
import {RouterView, useRoute} from 'vue-router';
import {Sidebar} from './sidebar';
import {ConfirmModal} from '@/confirm';
+import {ToastContainer} from '@/toast';
import {routeNames} from '@/routes';
const route = useRoute();
@@ -23,6 +24,7 @@ const hideSidebar = computed(() => {
+
diff --git a/src/toast/index.ts b/src/toast/index.ts
new file mode 100644
index 0000000..f746706
--- /dev/null
+++ b/src/toast/index.ts
@@ -0,0 +1,3 @@
+export { default as ToastContainer } from './ToastContainer.vue';
+export { toast, useToastStore } from './useToast';
+export type { ToastOptions, ToastType } from './useToast';
diff --git a/src/toast/useToast.ts b/src/toast/useToast.ts
new file mode 100644
index 0000000..8bd45d8
--- /dev/null
+++ b/src/toast/useToast.ts
@@ -0,0 +1,63 @@
+import {defineStore} from "pinia";
+import {ref} from "vue";
+
+export type ToastType = 'success' | 'error' | 'info';
+
+export interface ToastOptions {
+ message: string;
+ title?: string;
+ type?: ToastType;
+ duration?: number;
+}
+
+export interface Toast {
+ id: number;
+ message: string;
+ title?: string;
+ type: ToastType;
+}
+
+const DEFAULT_DURATION = 5000;
+
+export const useToastStore = defineStore('toast', () => {
+ const toasts = ref([]);
+ let nextId = 0;
+
+ const dismiss = (id: number) => {
+ toasts.value = toasts.value.filter(toast => toast.id !== id);
+ };
+
+ const push = (opts: ToastOptions | string): number => {
+ const options = typeof opts === "string" ? {message: opts} : opts;
+ const id = nextId++;
+ toasts.value.push({
+ id,
+ message: options.message,
+ title: options.title,
+ type: options.type ?? 'info',
+ });
+ const duration = options.duration ?? DEFAULT_DURATION;
+ if (duration > 0) {
+ setTimeout(() => dismiss(id), duration);
+ }
+ return id;
+ };
+
+ return {toasts, push, dismiss};
+});
+
+interface ToastHelper {
+ (opts: ToastOptions | string): number;
+ success: (message: string) => number;
+ error: (message: string) => number;
+ info: (message: string) => number;
+}
+
+export const toast: ToastHelper = Object.assign(
+ (opts: ToastOptions | string): number => useToastStore().push(opts),
+ {
+ success: (message: string): number => useToastStore().push({message, type: 'success'}),
+ error: (message: string): number => useToastStore().push({message, type: 'error'}),
+ info: (message: string): number => useToastStore().push({message, type: 'info'}),
+ },
+);