From 14ce457c7bd0409f84cb094f5f1e291711a6cf96 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:03:29 +0100 Subject: [PATCH] refactor(shared-ui): centralize toast system across all web apps - Add toast module to @manacore/shared-ui (toastStore, ToastContainer) - Remove local toast implementations from: - calendar/web - chat/web - clock/web - contacts/web - picture/web - storage/web - Update all apps to import toast from shared-ui - Consistent toast API: toast.success(), toast.error(), toast.info() Co-Authored-By: Claude Opus 4.5 --- .../src/lib/components/ToastContainer.svelte | 184 ----------------- .../components/event/EventContextMenu.svelte | 3 +- .../components/event/EventDetailModal.svelte | 3 +- .../components/event/QuickEventOverlay.svelte | 2 +- .../components/settings/SettingsModal.svelte | 2 +- .../components/todo/TodoDetailModal.svelte | 2 +- .../apps/web/src/lib/stores/events.svelte.ts | 2 +- .../apps/web/src/lib/stores/toast.svelte.ts | 57 ------ .../web/src/routes/(app)/mana/+page.svelte | 2 +- .../src/routes/(app)/settings/+page.svelte | 2 +- .../apps/web/src/routes/+layout.svelte | 3 +- .../apps/web/src/lib/components/Toast.svelte | 66 ------ .../src/lib/stores/conversations.svelte.ts | 2 +- .../apps/web/src/lib/stores/toast.svelte.ts | 103 ---------- .../routes/(protected)/chat/[id]/+page.svelte | 2 +- apps/chat/apps/web/src/routes/+layout.svelte | 4 +- .../src/lib/components/ToastContainer.svelte | 72 ------- apps/clock/apps/web/src/lib/stores/toast.ts | 47 ----- .../web/src/routes/(app)/alarms/+page.svelte | 3 +- .../web/src/routes/(app)/timers/+page.svelte | 3 +- .../src/routes/(app)/world-clock/+page.svelte | 3 +- apps/clock/apps/web/src/routes/+layout.svelte | 2 +- .../web/src/lib/components/ContactList.svelte | 14 +- .../src/lib/components/ToastContainer.svelte | 72 ------- .../contacts/apps/web/src/lib/stores/toast.ts | 44 ---- .../src/routes/(app)/duplicates/+page.svelte | 10 +- .../web/src/routes/(app)/mana/+page.svelte | 6 +- .../apps/web/src/routes/+layout.svelte | 11 +- .../components/board/ImagePickerModal.svelte | 11 +- .../board/ImagePropertiesPanel.svelte | 16 +- .../gallery/ImageDetailModal.svelte | 28 +-- .../gallery/QuickGenerateBar.svelte | 8 +- .../src/lib/components/ui/ContextMenu.svelte | 28 +-- .../web/src/lib/components/ui/Toast.svelte | 53 ----- .../apps/web/src/lib/stores/toast.svelte.ts | 80 -------- apps/picture/apps/web/src/lib/stores/toast.ts | 37 ---- .../apps/web/src/routes/+layout.svelte | 4 +- .../web/src/routes/app/board/+page.svelte | 16 +- .../src/routes/app/board/[id]/+page.svelte | 10 +- .../apps/web/src/routes/app/mana/+page.svelte | 10 +- .../apps/web/src/routes/app/tags/+page.svelte | 16 +- .../web/src/routes/app/upload/+page.svelte | 13 +- .../src/lib/components/ToastContainer.svelte | 188 ------------------ apps/storage/apps/web/src/lib/stores/toast.ts | 63 ------ .../apps/web/src/routes/+layout.svelte | 2 +- .../web/src/routes/favorites/+page.svelte | 6 +- .../apps/web/src/routes/feedback/+page.svelte | 4 +- .../apps/web/src/routes/files/+page.svelte | 40 ++-- .../src/routes/files/[folderId]/+page.svelte | 40 ++-- .../apps/web/src/routes/shared/+page.svelte | 8 +- .../apps/web/src/routes/trash/+page.svelte | 14 +- packages/shared-ui/src/index.ts | 4 + .../organisms/network/NetworkControls.svelte | 23 ++- .../shared-ui/src/toast/ToastContainer.svelte | 139 +++++++++++++ packages/shared-ui/src/toast/index.ts | 3 + packages/shared-ui/src/toast/toast.svelte.ts | 146 ++++++++++++++ 56 files changed, 487 insertions(+), 1249 deletions(-) delete mode 100644 apps/calendar/apps/web/src/lib/components/ToastContainer.svelte delete mode 100644 apps/calendar/apps/web/src/lib/stores/toast.svelte.ts delete mode 100644 apps/chat/apps/web/src/lib/components/Toast.svelte delete mode 100644 apps/chat/apps/web/src/lib/stores/toast.svelte.ts delete mode 100644 apps/clock/apps/web/src/lib/components/ToastContainer.svelte delete mode 100644 apps/clock/apps/web/src/lib/stores/toast.ts delete mode 100644 apps/contacts/apps/web/src/lib/components/ToastContainer.svelte delete mode 100644 apps/contacts/apps/web/src/lib/stores/toast.ts delete mode 100644 apps/picture/apps/web/src/lib/components/ui/Toast.svelte delete mode 100644 apps/picture/apps/web/src/lib/stores/toast.svelte.ts delete mode 100644 apps/picture/apps/web/src/lib/stores/toast.ts delete mode 100644 apps/storage/apps/web/src/lib/components/ToastContainer.svelte delete mode 100644 apps/storage/apps/web/src/lib/stores/toast.ts create mode 100644 packages/shared-ui/src/toast/ToastContainer.svelte create mode 100644 packages/shared-ui/src/toast/index.ts create mode 100644 packages/shared-ui/src/toast/toast.svelte.ts diff --git a/apps/calendar/apps/web/src/lib/components/ToastContainer.svelte b/apps/calendar/apps/web/src/lib/components/ToastContainer.svelte deleted file mode 100644 index 8ef3a8637..000000000 --- a/apps/calendar/apps/web/src/lib/components/ToastContainer.svelte +++ /dev/null @@ -1,184 +0,0 @@ - - -
- {#each toasts as toastItem (toastItem.id)} - - {/each} -
- - diff --git a/apps/calendar/apps/web/src/lib/components/event/EventContextMenu.svelte b/apps/calendar/apps/web/src/lib/components/event/EventContextMenu.svelte index 0d7c36e2b..fc0d49e90 100644 --- a/apps/calendar/apps/web/src/lib/components/event/EventContextMenu.svelte +++ b/apps/calendar/apps/web/src/lib/components/event/EventContextMenu.svelte @@ -1,10 +1,9 @@ - -{#if toasts.length > 0} -
- {#each toasts as toast (toast.id)} - {@const Icon = icons[toast.type]} - - {/each} -
-{/if} - - diff --git a/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts b/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts index 7a12cf6e7..6e85b520d 100644 --- a/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts +++ b/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts @@ -4,7 +4,7 @@ */ import { conversationService } from '$lib/services/conversation'; -import { toastStore } from './toast.svelte'; +import { toastStore } from '@manacore/shared-ui'; import { sessionConversationsStore } from './session-conversations.svelte'; import { authStore } from './auth.svelte'; import type { Conversation } from '@chat/types'; diff --git a/apps/chat/apps/web/src/lib/stores/toast.svelte.ts b/apps/chat/apps/web/src/lib/stores/toast.svelte.ts deleted file mode 100644 index 5528237ff..000000000 --- a/apps/chat/apps/web/src/lib/stores/toast.svelte.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Toast Store - Centralized notification system using Svelte 5 runes - */ - -export type ToastType = 'success' | 'error' | 'warning' | 'info'; - -export interface Toast { - id: string; - type: ToastType; - message: string; - duration: number; -} - -// State -let toasts = $state([]); - -// Auto-incrementing ID -let nextId = 0; - -function generateId(): string { - return `toast-${++nextId}-${Date.now()}`; -} - -export const toastStore = { - // Getter for reading toasts - get toasts() { - return toasts; - }, - - /** - * Show a toast notification - */ - show(message: string, type: ToastType = 'info', duration: number = 4000) { - const id = generateId(); - const toast: Toast = { id, type, message, duration }; - - toasts = [...toasts, toast]; - - // Auto-remove after duration - if (duration > 0) { - setTimeout(() => { - this.dismiss(id); - }, duration); - } - - return id; - }, - - /** - * Show success toast - */ - success(message: string, duration?: number) { - return this.show(message, 'success', duration); - }, - - /** - * Show error toast - */ - error(message: string, duration: number = 6000) { - return this.show(message, 'error', duration); - }, - - /** - * Show warning toast - */ - warning(message: string, duration?: number) { - return this.show(message, 'warning', duration); - }, - - /** - * Show info toast - */ - info(message: string, duration?: number) { - return this.show(message, 'info', duration); - }, - - /** - * Dismiss a specific toast - */ - dismiss(id: string) { - toasts = toasts.filter((t) => t.id !== id); - }, - - /** - * Dismiss all toasts - */ - dismissAll() { - toasts = []; - }, -}; - -/** - * Helper function for API error handling - * Use this in services/stores to show user-friendly error messages - */ -export function handleApiError( - error: unknown, - fallbackMessage: string = 'Ein Fehler ist aufgetreten' -): string { - const message = error instanceof Error ? error.message : fallbackMessage; - toastStore.error(message); - return message; -} diff --git a/apps/chat/apps/web/src/routes/(protected)/chat/[id]/+page.svelte b/apps/chat/apps/web/src/routes/(protected)/chat/[id]/+page.svelte index 5262047bf..c4cb78612 100644 --- a/apps/chat/apps/web/src/routes/(protected)/chat/[id]/+page.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/chat/[id]/+page.svelte @@ -5,7 +5,7 @@ import { documentService } from '$lib/services/document'; import { authStore } from '$lib/stores/auth.svelte'; import { conversationsStore } from '$lib/stores/conversations.svelte'; - import { toastStore } from '$lib/stores/toast.svelte'; + import { toastStore } from '@manacore/shared-ui'; import MessageList from '$lib/components/chat/MessageList.svelte'; import ChatInput from '$lib/components/chat/ChatInput.svelte'; import ChatLayout from '$lib/components/chat/ChatLayout.svelte'; diff --git a/apps/chat/apps/web/src/routes/+layout.svelte b/apps/chat/apps/web/src/routes/+layout.svelte index cb7ce45ce..1e6853364 100644 --- a/apps/chat/apps/web/src/routes/+layout.svelte +++ b/apps/chat/apps/web/src/routes/+layout.svelte @@ -2,7 +2,7 @@ import '../app.css'; import { onMount } from 'svelte'; import { theme } from '$lib/stores/theme'; - import Toast from '$lib/components/Toast.svelte'; + import { ToastContainer } from '@manacore/shared-ui'; let { children } = $props(); @@ -17,4 +17,4 @@ - + diff --git a/apps/clock/apps/web/src/lib/components/ToastContainer.svelte b/apps/clock/apps/web/src/lib/components/ToastContainer.svelte deleted file mode 100644 index 28d60f24c..000000000 --- a/apps/clock/apps/web/src/lib/components/ToastContainer.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - -
- {#each $toasts as toast (toast.id)} -
- - {getIcon(toast.type)} - - {toast.message} - -
- {/each} -
- - diff --git a/apps/clock/apps/web/src/lib/stores/toast.ts b/apps/clock/apps/web/src/lib/stores/toast.ts deleted file mode 100644 index 61dc340b0..000000000 --- a/apps/clock/apps/web/src/lib/stores/toast.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { writable } from 'svelte/store'; - -export interface Toast { - id: string; - type: 'success' | 'error' | 'info' | 'warning'; - message: string; - duration?: number; -} - -function createToastStore() { - const { subscribe, update } = writable([]); - - function addToast(toast: Omit) { - const id = crypto.randomUUID(); - const newToast = { ...toast, id }; - - update((toasts) => [...toasts, newToast]); - - // Auto-remove after duration - const duration = toast.duration || 5000; - setTimeout(() => { - removeToast(id); - }, duration); - - return id; - } - - function removeToast(id: string) { - update((toasts) => toasts.filter((t) => t.id !== id)); - } - - return { - subscribe, - success: (message: string, duration?: number) => - addToast({ type: 'success', message, duration }), - error: (message: string, duration?: number) => addToast({ type: 'error', message, duration }), - info: (message: string, duration?: number) => addToast({ type: 'info', message, duration }), - warning: (message: string, duration?: number) => - addToast({ type: 'warning', message, duration }), - remove: removeToast, - }; -} - -export const toasts = createToastStore(); - -// Alias for compatibility with different import styles -export const toast = toasts; diff --git a/apps/clock/apps/web/src/routes/(app)/alarms/+page.svelte b/apps/clock/apps/web/src/routes/(app)/alarms/+page.svelte index ae6f686c7..d96cb6f09 100644 --- a/apps/clock/apps/web/src/routes/(app)/alarms/+page.svelte +++ b/apps/clock/apps/web/src/routes/(app)/alarms/+page.svelte @@ -1,10 +1,9 @@ - -
- {#each $toasts as toast (toast.id)} -
- - {getIcon(toast.type)} - - {toast.message} - -
- {/each} -
- - diff --git a/apps/contacts/apps/web/src/lib/stores/toast.ts b/apps/contacts/apps/web/src/lib/stores/toast.ts deleted file mode 100644 index eb45c4b3c..000000000 --- a/apps/contacts/apps/web/src/lib/stores/toast.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { writable } from 'svelte/store'; - -export interface Toast { - id: string; - type: 'success' | 'error' | 'info' | 'warning'; - message: string; - duration?: number; -} - -function createToastStore() { - const { subscribe, update } = writable([]); - - function addToast(toast: Omit) { - const id = crypto.randomUUID(); - const newToast = { ...toast, id }; - - update((toasts) => [...toasts, newToast]); - - // Auto-remove after duration - const duration = toast.duration || 5000; - setTimeout(() => { - removeToast(id); - }, duration); - - return id; - } - - function removeToast(id: string) { - update((toasts) => toasts.filter((t) => t.id !== id)); - } - - return { - subscribe, - success: (message: string, duration?: number) => - addToast({ type: 'success', message, duration }), - error: (message: string, duration?: number) => addToast({ type: 'error', message, duration }), - info: (message: string, duration?: number) => addToast({ type: 'info', message, duration }), - warning: (message: string, duration?: number) => - addToast({ type: 'warning', message, duration }), - remove: removeToast, - }; -} - -export const toasts = createToastStore(); diff --git a/apps/contacts/apps/web/src/routes/(app)/duplicates/+page.svelte b/apps/contacts/apps/web/src/routes/(app)/duplicates/+page.svelte index eb4901f1b..88f90c783 100644 --- a/apps/contacts/apps/web/src/routes/(app)/duplicates/+page.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/duplicates/+page.svelte @@ -4,7 +4,7 @@ import { duplicatesApi, type DuplicateGroup } from '$lib/api/duplicates'; import MergeModal from '$lib/components/duplicates/MergeModal.svelte'; import { DuplicateListSkeleton } from '$lib/components/skeletons'; - import { toasts } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; let duplicates = $state([]); let loading = $state(true); @@ -75,14 +75,14 @@ async function handleMerge(primaryId: string, mergeIds: string[]) { try { await duplicatesApi.mergeContacts(primaryId, mergeIds); - toasts.success(`${mergeIds.length + 1} Kontakte wurden zusammengeführt`); + toastStore.success(`${mergeIds.length + 1} Kontakte wurden zusammengeführt`); // Remove the merged group from the list if (selectedGroup) { duplicates = duplicates.filter((d) => d.id !== selectedGroup!.id); } handleCloseMergeModal(); } catch (e) { - toasts.error(e instanceof Error ? e.message : 'Fehler beim Zusammenführen'); + toastStore.error(e instanceof Error ? e.message : 'Fehler beim Zusammenführen'); } } @@ -91,10 +91,10 @@ try { await duplicatesApi.dismissDuplicate(selectedGroup.id); duplicates = duplicates.filter((d) => d.id !== selectedGroup!.id); - toasts.info('Duplikat-Gruppe wurde ignoriert'); + toastStore.info('Duplikat-Gruppe wurde ignoriert'); handleCloseMergeModal(); } catch (e) { - toasts.error('Fehler beim Ignorieren'); + toastStore.error('Fehler beim Ignorieren'); } } diff --git a/apps/contacts/apps/web/src/routes/(app)/mana/+page.svelte b/apps/contacts/apps/web/src/routes/(app)/mana/+page.svelte index a3e5f7e91..b2c97a600 100644 --- a/apps/contacts/apps/web/src/routes/(app)/mana/+page.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/mana/+page.svelte @@ -1,15 +1,15 @@ diff --git a/apps/contacts/apps/web/src/routes/+layout.svelte b/apps/contacts/apps/web/src/routes/+layout.svelte index 27f8460a2..35cbef0d2 100644 --- a/apps/contacts/apps/web/src/routes/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/+layout.svelte @@ -6,8 +6,7 @@ import { isLoading as i18nLoading } from 'svelte-i18n'; import { theme } from '$lib/stores/theme'; import { authStore } from '$lib/stores/auth.svelte'; - import { toasts } from '$lib/stores/toast'; - import ToastContainer from '$lib/components/ToastContainer.svelte'; + import { toastStore, ToastContainer } from '@manacore/shared-ui'; import { AppLoadingSkeleton } from '$lib/components/skeletons'; let { children } = $props(); @@ -49,7 +48,7 @@ } // Show toast notification - toasts.error(message); + toastStore.error(message); // Prevent default browser error handling event.preventDefault(); @@ -59,17 +58,17 @@ window.addEventListener('error', (event) => { // Only handle non-script errors (network failures for resources, etc.) if (event.message && !event.filename) { - toasts.error('Ein Fehler ist aufgetreten'); + toastStore.error('Ein Fehler ist aufgetreten'); } }); // Handle offline/online status window.addEventListener('offline', () => { - toasts.warning('Keine Internetverbindung', 10000); + toastStore.warning('Keine Internetverbindung', 10000); }); window.addEventListener('online', () => { - toasts.success('Verbindung wiederhergestellt'); + toastStore.success('Verbindung wiederhergestellt'); }); } diff --git a/apps/picture/apps/web/src/lib/components/board/ImagePickerModal.svelte b/apps/picture/apps/web/src/lib/components/board/ImagePickerModal.svelte index 10738f5e6..187c29141 100644 --- a/apps/picture/apps/web/src/lib/components/board/ImagePickerModal.svelte +++ b/apps/picture/apps/web/src/lib/components/board/ImagePickerModal.svelte @@ -5,7 +5,7 @@ import { canvasItems, addCanvasItem } from '$lib/stores/canvas'; import { getImages } from '$lib/api/images'; import { addBoardItem } from '$lib/api/boardItems'; - import { showToast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import Modal from '$lib/components/ui/Modal.svelte'; import Button from '$lib/components/ui/Button.svelte'; import { MagnifyingGlass, Image as ImageIcon, Check } from '@manacore/shared-icons'; @@ -47,7 +47,7 @@ hasMore = data.length === 50; } catch (error) { console.error('Error loading images:', error); - showToast('Fehler beim Laden der Bilder', 'error'); + toastStore.show('Fehler beim Laden der Bilder', 'error'); } finally { isLoadingImages.set(false); } @@ -104,11 +104,14 @@ selectedImages.clear(); selectedImages = new Set(); - showToast(`${addedCount} ${addedCount === 1 ? 'Bild' : 'Bilder'} hinzugefügt`, 'success'); + toastStore.show( + `${addedCount} ${addedCount === 1 ? 'Bild' : 'Bilder'} hinzugefügt`, + 'success' + ); onClose(); } catch (error) { console.error('Error adding images:', error); - showToast('Fehler beim Hinzufügen', 'error'); + toastStore.show('Fehler beim Hinzufügen', 'error'); } finally { isAdding = false; } diff --git a/apps/picture/apps/web/src/lib/components/board/ImagePropertiesPanel.svelte b/apps/picture/apps/web/src/lib/components/board/ImagePropertiesPanel.svelte index 4e6c8aeb3..f2e914ef5 100644 --- a/apps/picture/apps/web/src/lib/components/board/ImagePropertiesPanel.svelte +++ b/apps/picture/apps/web/src/lib/components/board/ImagePropertiesPanel.svelte @@ -2,7 +2,7 @@ import { selectedItems, updateCanvasItem, removeSelectedItems } from '$lib/stores/canvas'; import { updateBoardItem, changeBoardItemZIndex, isImageItem } from '$lib/api/boardItems'; import Button from '$lib/components/ui/Button.svelte'; - import { showToast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import { Image, CaretDoubleUp, @@ -51,7 +51,7 @@ await updateBoardItem(selectedItem.id, updates); } catch (error) { console.error('Error updating position:', error); - showToast('Fehler beim Speichern', 'error'); + toastStore.show('Fehler beim Speichern', 'error'); } } @@ -75,7 +75,7 @@ await updateBoardItem(selectedItem.id, updates); } catch (error) { console.error('Error updating scale:', error); - showToast('Fehler beim Speichern', 'error'); + toastStore.show('Fehler beim Speichern', 'error'); } } @@ -89,7 +89,7 @@ await updateBoardItem(selectedItem.id, updates); } catch (error) { console.error('Error updating rotation:', error); - showToast('Fehler beim Speichern', 'error'); + toastStore.show('Fehler beim Speichern', 'error'); } } @@ -104,7 +104,7 @@ await updateBoardItem(selectedItem.id, updates); } catch (error) { console.error('Error updating opacity:', error); - showToast('Fehler beim Speichern', 'error'); + toastStore.show('Fehler beim Speichern', 'error'); } } @@ -113,10 +113,10 @@ try { await changeBoardItemZIndex(selectedItem.id, direction); - showToast('Layer-Reihenfolge geändert', 'success'); + toastStore.show('Layer-Reihenfolge geändert', 'success'); } catch (error) { console.error('Error changing layer:', error); - showToast('Fehler beim Ändern der Layer-Reihenfolge', 'error'); + toastStore.show('Fehler beim Ändern der Layer-Reihenfolge', 'error'); } } @@ -139,7 +139,7 @@ function handleDelete() { removeSelectedItems(); - showToast('Bild entfernt', 'success'); + toastStore.show('Bild entfernt', 'success'); } diff --git a/apps/picture/apps/web/src/lib/components/gallery/ImageDetailModal.svelte b/apps/picture/apps/web/src/lib/components/gallery/ImageDetailModal.svelte index fa188bab2..4c4a0ec93 100644 --- a/apps/picture/apps/web/src/lib/components/gallery/ImageDetailModal.svelte +++ b/apps/picture/apps/web/src/lib/components/gallery/ImageDetailModal.svelte @@ -9,7 +9,7 @@ unpublishImage, } from '$lib/api/images'; import { images, selectedImage } from '$lib/stores/images'; - import { showToast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import { fade, fly } from 'svelte/transition'; import { getImageTags, getAllTags, addTagToImage, removeTagFromImage } from '$lib/api/tags'; import { @@ -104,11 +104,11 @@ await archiveImage(imageId); // Update store images.update((current) => current.filter((img) => img.id !== imageId)); - showToast('Bild erfolgreich archiviert', 'success'); + toastStore.show('Bild erfolgreich archiviert', 'success'); onClose(); } catch (error) { console.error('Error archiving image:', error); - showToast('Fehler beim Archivieren des Bildes', 'error'); + toastStore.show('Fehler beim Archivieren des Bildes', 'error'); } finally { isArchiving = false; } @@ -129,11 +129,11 @@ await deleteImage(imageId); // Update store images.update((current) => current.filter((img) => img.id !== imageId)); - showToast('Bild erfolgreich gelöscht', 'success'); + toastStore.show('Bild erfolgreich gelöscht', 'success'); onClose(); } catch (error) { console.error('Error deleting image:', error); - showToast('Fehler beim Löschen des Bildes', 'error'); + toastStore.show('Fehler beim Löschen des Bildes', 'error'); } finally { isDeleting = false; } @@ -143,7 +143,7 @@ if (!image || !image.publicUrl) return; const filename = `picture-${image.id}.png`; downloadImage(image.publicUrl, filename); - showToast('Download gestartet', 'success'); + toastStore.show('Download gestartet', 'success'); } function formatDate(dateString: string) { @@ -164,7 +164,7 @@ allTags = await getAllTags(); } catch (error) { console.error('Error loading tags:', error); - showToast('Fehler beim Laden der Tags', 'error'); + toastStore.show('Fehler beim Laden der Tags', 'error'); } finally { isLoadingTags = false; } @@ -183,15 +183,15 @@ if (isTagged) { await removeTagFromImage(image.id, tag.id); imageTags = imageTags.filter((t) => t.id !== tag.id); - showToast('Tag entfernt', 'success'); + toastStore.show('Tag entfernt', 'success'); } else { await addTagToImage(image.id, tag.id); imageTags = [...imageTags, tag]; - showToast('Tag hinzugefügt', 'success'); + toastStore.show('Tag hinzugefügt', 'success'); } } catch (error) { console.error('Error toggling tag:', error); - showToast('Fehler beim Aktualisieren des Tags', 'error'); + toastStore.show('Fehler beim Aktualisieren des Tags', 'error'); } } @@ -213,11 +213,11 @@ if (image) { image = { ...image, isPublic: true }; } - showToast('Bild erfolgreich veröffentlicht!', 'success'); + toastStore.show('Bild erfolgreich veröffentlicht!', 'success'); closePublishModal(); } catch (error) { console.error('Error publishing image:', error); - showToast('Fehler beim Veröffentlichen des Bildes', 'error'); + toastStore.show('Fehler beim Veröffentlichen des Bildes', 'error'); } finally { isPublishing = false; } @@ -233,11 +233,11 @@ if (image) { image = { ...image, isPublic: false }; } - showToast('Bild nicht mehr öffentlich', 'success'); + toastStore.show('Bild nicht mehr öffentlich', 'success'); closePublishModal(); } catch (error) { console.error('Error unpublishing image:', error); - showToast('Fehler beim Entfernen der Veröffentlichung', 'error'); + toastStore.show('Fehler beim Entfernen der Veröffentlichung', 'error'); } finally { isPublishing = false; } diff --git a/apps/picture/apps/web/src/lib/components/gallery/QuickGenerateBar.svelte b/apps/picture/apps/web/src/lib/components/gallery/QuickGenerateBar.svelte index c79742684..ce51f215e 100644 --- a/apps/picture/apps/web/src/lib/components/gallery/QuickGenerateBar.svelte +++ b/apps/picture/apps/web/src/lib/components/gallery/QuickGenerateBar.svelte @@ -6,7 +6,7 @@ import { isSidebarCollapsed } from '$lib/stores/sidebar'; import { getActiveModels } from '$lib/api/models'; import { generateImageAsync, subscribeToGenerationUpdates } from '$lib/api/generate-async'; - import { showToast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import { onMount } from 'svelte'; import AdvancedSettingsModal, { type AdvancedSettings, @@ -59,7 +59,7 @@ } } catch (error) { console.error('Error loading models:', error); - showToast('Fehler beim Laden der Modelle', 'error'); + toastStore.show('Fehler beim Laden der Modelle', 'error'); } finally { isLoadingModels.set(false); } @@ -124,7 +124,7 @@ // Success generationProgress.set('Fertig!'); - showToast( + toastStore.show( totalImages > 1 ? `${totalImages} Bilder erfolgreich generiert!` : 'Bild erfolgreich generiert!', @@ -137,7 +137,7 @@ console.error('Generation error:', error); const errorMessage = error instanceof Error ? error.message : 'Generierung fehlgeschlagen'; generationError.set(errorMessage); - showToast(errorMessage, 'error'); + toastStore.show(errorMessage, 'error'); } finally { setTimeout(() => { isGenerating.set(false); diff --git a/apps/picture/apps/web/src/lib/components/ui/ContextMenu.svelte b/apps/picture/apps/web/src/lib/components/ui/ContextMenu.svelte index 5064c92c9..3a4262789 100644 --- a/apps/picture/apps/web/src/lib/components/ui/ContextMenu.svelte +++ b/apps/picture/apps/web/src/lib/components/ui/ContextMenu.svelte @@ -12,7 +12,7 @@ import { archiveImage, unarchiveImage, deleteImage, toggleFavorite } from '$lib/api/images'; import { images } from '$lib/stores/images'; import { archivedImages } from '$lib/stores/archive'; - import { showToast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import type { Tag } from '$lib/api/tags'; import { DownloadSimple, @@ -76,10 +76,10 @@ try { await addTagToImage($contextMenu.image.id, tag.id); await loadImageTags($contextMenu.image.id); - showToast(`Tag "${tag.name}" hinzugefügt`, 'success'); + toastStore.show(`Tag "${tag.name}" hinzugefügt`, 'success'); } catch (error) { console.error('Error adding tag:', error); - showToast('Fehler beim Hinzufügen des Tags', 'error'); + toastStore.show('Fehler beim Hinzufügen des Tags', 'error'); } } @@ -89,10 +89,10 @@ try { await removeTagFromImage($contextMenu.image.id, tag.id); await loadImageTags($contextMenu.image.id); - showToast(`Tag "${tag.name}" entfernt`, 'success'); + toastStore.show(`Tag "${tag.name}" entfernt`, 'success'); } catch (error) { console.error('Error removing tag:', error); - showToast('Fehler beim Entfernen des Tags', 'error'); + toastStore.show('Fehler beim Entfernen des Tags', 'error'); } } @@ -104,7 +104,7 @@ link.download = $contextMenu.image.filename || 'image.png'; link.click(); hideContextMenu(); - showToast('Download gestartet', 'success'); + toastStore.show('Download gestartet', 'success'); } function handleCopyLink() { @@ -112,7 +112,7 @@ navigator.clipboard.writeText($contextMenu.image.publicUrl); hideContextMenu(); - showToast('Link kopiert', 'success'); + toastStore.show('Link kopiert', 'success'); } async function handleDelete() { @@ -124,10 +124,10 @@ // Remove from store images.update((current) => current.filter((img) => img.id !== $contextMenu.image?.id)); hideContextMenu(); - showToast('Bild gelöscht', 'success'); + toastStore.show('Bild gelöscht', 'success'); } catch (error) { console.error('Error deleting image:', error); - showToast('Fehler beim Löschen des Bildes', 'error'); + toastStore.show('Fehler beim Löschen des Bildes', 'error'); } } } @@ -144,18 +144,18 @@ current.filter((img) => img.id !== $contextMenu.image?.id) ); hideContextMenu(); - showToast('Bild wiederhergestellt', 'success'); + toastStore.show('Bild wiederhergestellt', 'success'); } else { // Archive: Move to archive await archiveImage($contextMenu.image.id); // Remove from gallery store images.update((current) => current.filter((img) => img.id !== $contextMenu.image?.id)); hideContextMenu(); - showToast('Bild archiviert', 'success'); + toastStore.show('Bild archiviert', 'success'); } } catch (error) { console.error('Error archiving/unarchiving image:', error); - showToast('Fehler beim Archivieren des Bildes', 'error'); + toastStore.show('Fehler beim Archivieren des Bildes', 'error'); } } @@ -179,13 +179,13 @@ ); hideContextMenu(); - showToast( + toastStore.show( newFavoriteStatus ? 'Zu Favoriten hinzugefügt' : 'Aus Favoriten entfernt', 'success' ); } catch (error) { console.error('Error toggling favorite:', error); - showToast('Fehler beim Aktualisieren der Favoriten', 'error'); + toastStore.show('Fehler beim Aktualisieren der Favoriten', 'error'); } } diff --git a/apps/picture/apps/web/src/lib/components/ui/Toast.svelte b/apps/picture/apps/web/src/lib/components/ui/Toast.svelte deleted file mode 100644 index 9ee618233..000000000 --- a/apps/picture/apps/web/src/lib/components/ui/Toast.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - -
- {#each $toasts as toast (toast.id)} - {@const bgColor = getToastBgColor(toast.type)} - - {/each} -
diff --git a/apps/picture/apps/web/src/lib/stores/toast.svelte.ts b/apps/picture/apps/web/src/lib/stores/toast.svelte.ts deleted file mode 100644 index 02382f895..000000000 --- a/apps/picture/apps/web/src/lib/stores/toast.svelte.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Toast Store - Svelte 5 Runes Version - */ - -export type ToastType = 'success' | 'error' | 'info' | 'warning'; - -export interface Toast { - id: string; - message: string; - type: ToastType; - duration?: number; -} - -let toasts = $state([]); -let toastId = 0; - -export const toastStore = { - get toasts() { - return toasts; - }, - - show(message: string, type: ToastType = 'info', duration = 5000): string { - const id = `toast-${toastId++}`; - const toast: Toast = { id, message, type, duration }; - - toasts = [...toasts, toast]; - - if (duration > 0) { - setTimeout(() => { - toastStore.dismiss(id); - }, duration); - } - - return id; - }, - - dismiss(id: string) { - toasts = toasts.filter((toast) => toast.id !== id); - }, - - clear() { - toasts = []; - }, - - success(message: string, duration = 5000) { - return toastStore.show(message, 'success', duration); - }, - - error(message: string, duration = 5000) { - return toastStore.show(message, 'error', duration); - }, - - warning(message: string, duration = 5000) { - return toastStore.show(message, 'warning', duration); - }, - - info(message: string, duration = 5000) { - return toastStore.show(message, 'info', duration); - }, -}; - -// Export for backwards compatibility -export function showToast(message: string, type: ToastType = 'info', duration = 5000) { - return toastStore.show(message, type, duration); -} - -export function dismissToast(id: string) { - toastStore.dismiss(id); -} - -export function clearToasts() { - toastStore.clear(); -} - -export function getToasts() { - return toasts; -} - -// Re-export for compatibility -export { toasts }; diff --git a/apps/picture/apps/web/src/lib/stores/toast.ts b/apps/picture/apps/web/src/lib/stores/toast.ts deleted file mode 100644 index daffc4f28..000000000 --- a/apps/picture/apps/web/src/lib/stores/toast.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { writable } from 'svelte/store'; - -export type ToastType = 'success' | 'error' | 'info' | 'warning'; - -export interface Toast { - id: string; - message: string; - type: ToastType; - duration?: number; -} - -export const toasts = writable([]); - -let toastId = 0; - -export function showToast(message: string, type: ToastType = 'info', duration = 5000) { - const id = `toast-${toastId++}`; - const toast: Toast = { id, message, type, duration }; - - toasts.update((current) => [...current, toast]); - - if (duration > 0) { - setTimeout(() => { - dismissToast(id); - }, duration); - } - - return id; -} - -export function dismissToast(id: string) { - toasts.update((current) => current.filter((toast) => toast.id !== id)); -} - -export function clearToasts() { - toasts.set([]); -} diff --git a/apps/picture/apps/web/src/routes/+layout.svelte b/apps/picture/apps/web/src/routes/+layout.svelte index 65f8926c9..10de2e8ec 100644 --- a/apps/picture/apps/web/src/routes/+layout.svelte +++ b/apps/picture/apps/web/src/routes/+layout.svelte @@ -2,7 +2,7 @@ import '../app.css'; import favicon from '$lib/assets/favicon.svg'; import { authStore } from '$lib/stores/auth.svelte'; - import Toast from '$lib/components/ui/Toast.svelte'; + import { ToastContainer } from '@manacore/shared-ui'; import { onMount } from 'svelte'; // Import and initialize theme @@ -43,4 +43,4 @@ {@render children?.()} - + diff --git a/apps/picture/apps/web/src/routes/app/board/+page.svelte b/apps/picture/apps/web/src/routes/app/board/+page.svelte index c87e06b43..3907416bf 100644 --- a/apps/picture/apps/web/src/routes/app/board/+page.svelte +++ b/apps/picture/apps/web/src/routes/app/board/+page.svelte @@ -17,7 +17,7 @@ import { PageHeader } from '@manacore/shared-ui'; import Button from '$lib/components/ui/Button.svelte'; import Modal from '$lib/components/ui/Modal.svelte'; - import { showToast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import { Plus, SquaresFour, Image, Trash } from '@manacore/shared-icons'; let loadingMore = $state(false); @@ -70,7 +70,7 @@ hasBoardsMore.set(data.length === 20); } catch (error) { console.error('Error loading boards:', error); - showToast('Fehler beim Laden der Boards', 'error'); + toastStore.show('Fehler beim Laden der Boards', 'error'); } finally { isLoadingBoards.set(false); } @@ -112,10 +112,10 @@ showCreateBoardModal.set(false); boardName = ''; boardDescription = ''; - showToast('Board erstellt', 'success'); + toastStore.show('Board erstellt', 'success'); } catch (error) { console.error('Error creating board:', error); - showToast('Fehler beim Erstellen', 'error'); + toastStore.show('Fehler beim Erstellen', 'error'); } finally { isCreating = false; } @@ -129,10 +129,10 @@ removeBoardFromList(deletingBoard); showDeleteModal = false; deletingBoard = null; - showToast('Board gelöscht', 'success'); + toastStore.show('Board gelöscht', 'success'); } catch (error) { console.error('Error deleting board:', error); - showToast('Fehler beim Löschen', 'error'); + toastStore.show('Fehler beim Löschen', 'error'); } } @@ -142,10 +142,10 @@ try { const newBoard = await duplicateBoard(boardId); addBoard({ ...newBoard, itemCount: 0 }); - showToast('Board dupliziert', 'success'); + toastStore.show('Board dupliziert', 'success'); } catch (error) { console.error('Error duplicating board:', error); - showToast('Fehler beim Duplizieren', 'error'); + toastStore.show('Fehler beim Duplizieren', 'error'); } } diff --git a/apps/picture/apps/web/src/routes/app/board/[id]/+page.svelte b/apps/picture/apps/web/src/routes/app/board/[id]/+page.svelte index 7ca6d069d..84139ff79 100644 --- a/apps/picture/apps/web/src/routes/app/board/[id]/+page.svelte +++ b/apps/picture/apps/web/src/routes/app/board/[id]/+page.svelte @@ -14,7 +14,7 @@ } from '$lib/stores/canvas'; import { getBoardById } from '$lib/api/boards'; import { getBoardItems, addTextToBoard } from '$lib/api/boardItems'; - import { showToast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import BoardCanvas from '$lib/components/board/BoardCanvas.svelte'; import CanvasToolbar from '$lib/components/board/CanvasToolbar.svelte'; import ImagePickerModal from '$lib/components/board/ImagePickerModal.svelte'; @@ -50,7 +50,7 @@ // Check if user has access if (board.userId !== authStore.user.id && !board.isPublic) { - showToast('Zugriff verweigert', 'error'); + toastStore.show('Zugriff verweigert', 'error'); goto('/app/board'); return; } @@ -63,7 +63,7 @@ canvasItems.set(items); } catch (error) { console.error('Error loading board:', error); - showToast('Fehler beim Laden des Boards', 'error'); + toastStore.show('Fehler beim Laden des Boards', 'error'); goto('/app/board'); } finally { isLoading = false; @@ -88,10 +88,10 @@ // Add to canvas addCanvasItem(text); - showToast('Text hinzugefügt', 'success'); + toastStore.show('Text hinzugefügt', 'success'); } catch (error) { console.error('Error adding text:', error); - showToast('Fehler beim Hinzufügen des Textes', 'error'); + toastStore.show('Fehler beim Hinzufügen des Textes', 'error'); } } diff --git a/apps/picture/apps/web/src/routes/app/mana/+page.svelte b/apps/picture/apps/web/src/routes/app/mana/+page.svelte index c47bf3433..4c20c9ab7 100644 --- a/apps/picture/apps/web/src/routes/app/mana/+page.svelte +++ b/apps/picture/apps/web/src/routes/app/mana/+page.svelte @@ -1,15 +1,19 @@ diff --git a/apps/picture/apps/web/src/routes/app/tags/+page.svelte b/apps/picture/apps/web/src/routes/app/tags/+page.svelte index 63a55e58c..9c4d0e398 100644 --- a/apps/picture/apps/web/src/routes/app/tags/+page.svelte +++ b/apps/picture/apps/web/src/routes/app/tags/+page.svelte @@ -3,7 +3,7 @@ import { tags, isLoadingTags } from '$lib/stores/tags'; import { getAllTags, createTag, updateTag, deleteTag } from '$lib/api/tags'; import type { Tag } from '$lib/api/tags'; - import { showToast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import { PageHeader } from '@manacore/shared-ui'; import { Plus, Tag as TagIcon, PencilSimple, Trash } from '@manacore/shared-icons'; @@ -37,7 +37,7 @@ tags.set(data); } catch (error) { console.error('Error loading tags:', error); - showToast('Fehler beim Laden der Tags', 'error'); + toastStore.show('Fehler beim Laden der Tags', 'error'); } finally { isLoadingTags.set(false); } @@ -52,13 +52,13 @@ color: newTagColor, }); await loadTags(); - showToast('Tag erfolgreich erstellt', 'success'); + toastStore.show('Tag erfolgreich erstellt', 'success'); newTagName = ''; newTagColor = '#3B82F6'; showCreateModal = false; } catch (error) { console.error('Error creating tag:', error); - showToast('Fehler beim Erstellen des Tags', 'error'); + toastStore.show('Fehler beim Erstellen des Tags', 'error'); } } @@ -78,12 +78,12 @@ color: editTagColor, }); await loadTags(); - showToast('Tag erfolgreich aktualisiert', 'success'); + toastStore.show('Tag erfolgreich aktualisiert', 'success'); showEditModal = false; editingTag = null; } catch (error) { console.error('Error updating tag:', error); - showToast('Fehler beim Aktualisieren des Tags', 'error'); + toastStore.show('Fehler beim Aktualisieren des Tags', 'error'); } } @@ -93,10 +93,10 @@ try { await deleteTag(tagId); await loadTags(); - showToast('Tag erfolgreich gelöscht', 'success'); + toastStore.show('Tag erfolgreich gelöscht', 'success'); } catch (error) { console.error('Error deleting tag:', error); - showToast('Fehler beim Löschen des Tags', 'error'); + toastStore.show('Fehler beim Löschen des Tags', 'error'); } } diff --git a/apps/picture/apps/web/src/routes/app/upload/+page.svelte b/apps/picture/apps/web/src/routes/app/upload/+page.svelte index 7775d878d..1b92ef97b 100644 --- a/apps/picture/apps/web/src/routes/app/upload/+page.svelte +++ b/apps/picture/apps/web/src/routes/app/upload/+page.svelte @@ -3,7 +3,7 @@ import { goto } from '$app/navigation'; import { uploadMultipleImages } from '$lib/api/upload'; import type { UploadProgress } from '$lib/api/upload'; - import { showToast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import { PageHeader } from '@manacore/shared-ui'; import DropZone from '$lib/components/upload/DropZone.svelte'; import { images } from '$lib/stores/images'; @@ -15,7 +15,7 @@ async function handleFilesSelected(files: File[]) { if (!authStore.user) { - showToast('Bitte melde dich an', 'error'); + toastStore.show('Bitte melde dich an', 'error'); return; } @@ -33,12 +33,15 @@ images.update((current) => [...uploadedImages, ...current]); if (successCount === files.length) { - showToast( + toastStore.show( `${successCount} ${successCount === 1 ? 'Bild' : 'Bilder'} erfolgreich hochgeladen`, 'success' ); } else { - showToast(`${successCount} von ${files.length} Bildern erfolgreich hochgeladen`, 'warning'); + toastStore.show( + `${successCount} von ${files.length} Bildern erfolgreich hochgeladen`, + 'warning' + ); } // Redirect to gallery after successful upload @@ -47,7 +50,7 @@ }, 2000); } catch (error) { console.error('Upload error:', error); - showToast('Fehler beim Hochladen der Bilder', 'error'); + toastStore.show('Fehler beim Hochladen der Bilder', 'error'); } finally { uploading = false; } diff --git a/apps/storage/apps/web/src/lib/components/ToastContainer.svelte b/apps/storage/apps/web/src/lib/components/ToastContainer.svelte deleted file mode 100644 index 3019a2990..000000000 --- a/apps/storage/apps/web/src/lib/components/ToastContainer.svelte +++ /dev/null @@ -1,188 +0,0 @@ - - -
- {#each toasts as toastItem (toastItem.id)} - - {/each} -
- - diff --git a/apps/storage/apps/web/src/lib/stores/toast.ts b/apps/storage/apps/web/src/lib/stores/toast.ts deleted file mode 100644 index c7f1bd9c8..000000000 --- a/apps/storage/apps/web/src/lib/stores/toast.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Toast Store - Manages toast notifications - */ - -import { writable } from 'svelte/store'; - -export interface Toast { - id: string; - type: 'success' | 'error' | 'warning' | 'info'; - message: string; - duration?: number; -} - -function createToastStore() { - const { subscribe, update } = writable([]); - - function add(toast: Omit) { - const id = crypto.randomUUID(); - const duration = toast.duration ?? 5000; - - update((toasts) => [...toasts, { ...toast, id }]); - - if (duration > 0) { - setTimeout(() => { - remove(id); - }, duration); - } - - return id; - } - - function remove(id: string) { - update((toasts) => toasts.filter((t) => t.id !== id)); - } - - function success(message: string, duration?: number) { - return add({ type: 'success', message, duration }); - } - - function error(message: string, duration?: number) { - return add({ type: 'error', message, duration }); - } - - function warning(message: string, duration?: number) { - return add({ type: 'warning', message, duration }); - } - - function info(message: string, duration?: number) { - return add({ type: 'info', message, duration }); - } - - return { - subscribe, - add, - remove, - success, - error, - warning, - info, - }; -} - -export const toast = createToastStore(); diff --git a/apps/storage/apps/web/src/routes/+layout.svelte b/apps/storage/apps/web/src/routes/+layout.svelte index 137cd9893..96addf775 100644 --- a/apps/storage/apps/web/src/routes/+layout.svelte +++ b/apps/storage/apps/web/src/routes/+layout.svelte @@ -16,7 +16,7 @@ import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n'; import { getPillAppItems } from '@manacore/shared-branding'; import { setLocale, supportedLocales } from '$lib/i18n'; - import ToastContainer from '$lib/components/ToastContainer.svelte'; + import { ToastContainer } from '@manacore/shared-ui'; import '../app.css'; // App switcher items diff --git a/apps/storage/apps/web/src/routes/favorites/+page.svelte b/apps/storage/apps/web/src/routes/favorites/+page.svelte index e08743c8c..15e2588cf 100644 --- a/apps/storage/apps/web/src/routes/favorites/+page.svelte +++ b/apps/storage/apps/web/src/routes/favorites/+page.svelte @@ -5,7 +5,7 @@ import { searchApi } from '$lib/api/client'; import type { StorageFile, StorageFolder } from '$lib/api/client'; import { filesStore } from '$lib/stores/files.svelte'; - import { toast } from '$lib/stores/toast'; + import { toastStore } from '@manacore/shared-ui'; import FileGrid from '$lib/components/files/FileGrid.svelte'; import FileList from '$lib/components/files/FileList.svelte'; @@ -47,7 +47,7 @@ const result = await filesStore.toggleFileFavorite(file.id); if (!result.error) { files = files.filter((f) => f.id !== file.id); - toast.success('Favorit entfernt'); + toastStore.success('Favorit entfernt'); } } } @@ -57,7 +57,7 @@ const result = await filesStore.toggleFolderFavorite(folder.id); if (!result.error) { folders = folders.filter((f) => f.id !== folder.id); - toast.success('Favorit entfernt'); + toastStore.success('Favorit entfernt'); } } } diff --git a/apps/storage/apps/web/src/routes/feedback/+page.svelte b/apps/storage/apps/web/src/routes/feedback/+page.svelte index 7346bb0be..379cf58d8 100644 --- a/apps/storage/apps/web/src/routes/feedback/+page.svelte +++ b/apps/storage/apps/web/src/routes/feedback/+page.svelte @@ -1,6 +1,6 @@ + +{#if toasts.length > 0} +
+ {#each toasts as toast (toast.id)} + {@const Icon = icons[toast.type]} + + {/each} +
+{/if} + + diff --git a/packages/shared-ui/src/toast/index.ts b/packages/shared-ui/src/toast/index.ts new file mode 100644 index 000000000..67db8c244 --- /dev/null +++ b/packages/shared-ui/src/toast/index.ts @@ -0,0 +1,3 @@ +export { toastStore, toast, handleApiError } from './toast.svelte'; +export type { Toast, ToastType } from './toast.svelte'; +export { default as ToastContainer } from './ToastContainer.svelte'; diff --git a/packages/shared-ui/src/toast/toast.svelte.ts b/packages/shared-ui/src/toast/toast.svelte.ts new file mode 100644 index 000000000..d02e60ba2 --- /dev/null +++ b/packages/shared-ui/src/toast/toast.svelte.ts @@ -0,0 +1,146 @@ +/** + * Toast Store - Centralized notification system using Svelte 5 runes + * + * Usage: + * ```ts + * import { toastStore } from '@manacore/shared-ui'; + * + * // Show notifications + * toastStore.success('Saved successfully'); + * toastStore.error('Something went wrong'); + * toastStore.warning('Please check your input'); + * toastStore.info('New update available'); + * + * // Manual control + * const id = toastStore.show('Custom message', 'info', 5000); + * toastStore.dismiss(id); + * toastStore.dismissAll(); + * ``` + */ + +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface Toast { + id: string; + type: ToastType; + message: string; + duration: number; +} + +// State +let toasts = $state([]); + +// Auto-incrementing ID with timestamp for uniqueness +let nextId = 0; + +function generateId(): string { + return `toast-${++nextId}-${Date.now()}`; +} + +export const toastStore = { + /** + * Get all active toasts (reactive) + */ + get toasts() { + return toasts; + }, + + /** + * Show a toast notification + * @param message - The message to display + * @param type - Toast type: 'success' | 'error' | 'warning' | 'info' + * @param duration - Duration in ms (0 = permanent, default: 4000) + * @returns The toast ID for manual dismissal + */ + show(message: string, type: ToastType = 'info', duration = 4000): string { + const id = generateId(); + const toast: Toast = { id, type, message, duration }; + + toasts = [...toasts, toast]; + + // Auto-remove after duration (unless permanent) + if (duration > 0) { + setTimeout(() => { + this.dismiss(id); + }, duration); + } + + return id; + }, + + /** + * Show a success toast (green) + * @param message - The message to display + * @param duration - Duration in ms (default: 4000) + */ + success(message: string, duration?: number): string { + return this.show(message, 'success', duration); + }, + + /** + * Show an error toast (red) - longer default duration + * @param message - The message to display + * @param duration - Duration in ms (default: 6000) + */ + error(message: string, duration = 6000): string { + return this.show(message, 'error', duration); + }, + + /** + * Show a warning toast (amber) + * @param message - The message to display + * @param duration - Duration in ms (default: 4000) + */ + warning(message: string, duration?: number): string { + return this.show(message, 'warning', duration); + }, + + /** + * Show an info toast (blue) + * @param message - The message to display + * @param duration - Duration in ms (default: 4000) + */ + info(message: string, duration?: number): string { + return this.show(message, 'info', duration); + }, + + /** + * Dismiss a specific toast by ID + * @param id - The toast ID to dismiss + */ + dismiss(id: string): void { + toasts = toasts.filter((t) => t.id !== id); + }, + + /** + * Dismiss all active toasts + */ + dismissAll(): void { + toasts = []; + }, +}; + +/** + * Helper function for API error handling + * Shows an error toast and returns the error message + * + * @example + * ```ts + * try { + * await api.save(data); + * } catch (error) { + * handleApiError(error, 'Could not save data'); + * } + * ``` + */ +export function handleApiError( + error: unknown, + fallbackMessage = 'Ein Fehler ist aufgetreten' +): string { + const message = error instanceof Error ? error.message : fallbackMessage; + toastStore.error(message); + return message; +} + +// Backwards compatible alias +export const toast = toastStore;