mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 22:19:39 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
6d0d9d4f67
commit
14ce457c7b
56 changed files with 487 additions and 1249 deletions
|
|
@ -1,184 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { toastStore, type Toast } from '$lib/stores/toast.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
// Reactive getter from the runes-based store
|
||||
let toasts = $derived(toastStore.toasts);
|
||||
|
||||
function handleClose(id: string) {
|
||||
toastStore.remove(id);
|
||||
}
|
||||
|
||||
function getIcon(type: Toast['type']) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
</svg>`;
|
||||
case 'error':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||
</svg>`;
|
||||
case 'warning':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>`;
|
||||
case 'info':
|
||||
default:
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="toast-container">
|
||||
{#each toasts as toastItem (toastItem.id)}
|
||||
<div
|
||||
class="toast toast-{toastItem.type}"
|
||||
transition:fly={{ y: 20, duration: 300 }}
|
||||
role="alert"
|
||||
>
|
||||
<div class="toast-icon">
|
||||
{@html getIcon(toastItem.type)}
|
||||
</div>
|
||||
<p class="toast-message">{toastItem.message}</p>
|
||||
<button
|
||||
class="toast-close"
|
||||
onclick={() => handleClose(toastItem.id)}
|
||||
aria-label="Close notification"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: hsl(var(--card));
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid hsl(var(--border));
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
pointer-events: auto;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
border-left: 4px solid hsl(142.1 76.2% 36.3%);
|
||||
}
|
||||
|
||||
.toast-success .toast-icon {
|
||||
color: hsl(142.1 76.2% 36.3%);
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-left: 4px solid hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.toast-error .toast-icon {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
border-left: 4px solid hsl(47.9 95.8% 53.1%);
|
||||
}
|
||||
|
||||
.toast-warning .toast-icon {
|
||||
color: hsl(47.9 95.8% 53.1%);
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left: 4px solid hsl(var(--primary));
|
||||
}
|
||||
|
||||
.toast-info .toast-icon {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: hsl(var(--muted-foreground));
|
||||
transition: all 150ms ease;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toast-container {
|
||||
bottom: 6rem;
|
||||
right: 1rem;
|
||||
left: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
|
||||
import { ContextMenu, type ContextMenuItem, toastStore } from '@manacore/shared-ui';
|
||||
import { Pencil, Copy, Trash, Palette, CalendarBlank, Export } from '@manacore/shared-icons';
|
||||
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { toastStore } from '$lib/stores/toast.svelte';
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import EventForm from './EventForm.svelte';
|
||||
import { TagBadge } from '@manacore/shared-ui';
|
||||
import { TagBadge, toastStore as toast } from '@manacore/shared-ui';
|
||||
import type { CalendarEvent, UpdateEventInput } from '@calendar/shared';
|
||||
import * as api from '$lib/api/events';
|
||||
import { format } from 'date-fns';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import type {
|
||||
LocationDetails,
|
||||
CalendarEvent,
|
||||
|
|
@ -16,6 +15,7 @@
|
|||
ContactAvatar,
|
||||
ConfirmationPopover,
|
||||
FilterDropdown,
|
||||
toastStore as toast,
|
||||
type FilterDropdownOption,
|
||||
} from '@manacore/shared-ui';
|
||||
import { Users } from '@manacore/shared-icons';
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import type { TimeFormat, AllDayDisplayMode, SttLanguage } from '$lib/stores/settings.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import {
|
||||
toastStore as toast,
|
||||
GlobalSettingsSection,
|
||||
SettingsSection,
|
||||
SettingsCard,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import type { Task, UpdateTaskInput, TaskPriority } from '$lib/api/todos';
|
||||
import { PRIORITY_LABELS, PRIORITY_COLORS } from '$lib/api/todos';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import { toastStore as toast } from '@manacore/shared-ui';
|
||||
import TodoCheckbox from './TodoCheckbox.svelte';
|
||||
import PriorityBadge from './PriorityBadge.svelte';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { CalendarEvent, CreateEventInput, UpdateEventInput } from '@calenda
|
|||
import * as api from '$lib/api/events';
|
||||
import { format, isWithinInterval, isSameDay } from 'date-fns';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import { toastStore } from './toast.svelte';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
import { authStore } from './auth.svelte';
|
||||
import { generateDemoEvents, isDemoEvent } from '$lib/data/demo-events';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
/**
|
||||
* Toast Store - Svelte 5 Runes version
|
||||
* Manages toast notifications
|
||||
*/
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
// State
|
||||
let toasts = $state<Toast[]>([]);
|
||||
|
||||
function add(message: string, type: ToastType = 'info', duration: number = 4000): string {
|
||||
const id = crypto.randomUUID();
|
||||
const toast: Toast = { id, type, message, duration };
|
||||
|
||||
toasts = [...toasts, toast];
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
remove(id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function remove(id: string) {
|
||||
toasts = toasts.filter((t) => t.id !== id);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
toasts = [];
|
||||
}
|
||||
|
||||
export const toastStore = {
|
||||
get toasts() {
|
||||
return toasts;
|
||||
},
|
||||
|
||||
add,
|
||||
remove,
|
||||
clear,
|
||||
|
||||
success: (message: string, duration?: number) => add(message, 'success', duration),
|
||||
error: (message: string, duration?: number) => add(message, 'error', duration ?? 6000),
|
||||
warning: (message: string, duration?: number) => add(message, 'warning', duration),
|
||||
info: (message: string, duration?: number) => add(message, 'info', duration),
|
||||
};
|
||||
|
||||
// Keep old export for backwards compatibility
|
||||
export const toast = toastStore;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import { toastStore as toast } from '@manacore/shared-ui';
|
||||
|
||||
function handleSubscribe(planId: string) {
|
||||
toast.info(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import type { TimeFormat, AllDayDisplayMode } from '$lib/stores/settings.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import {
|
||||
toastStore as toast,
|
||||
GlobalSettingsSection,
|
||||
SettingsSection,
|
||||
SettingsCard,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
import '$lib/i18n';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { toastStore } from '$lib/stores/toast.svelte';
|
||||
import ToastContainer from '$lib/components/ToastContainer.svelte';
|
||||
import { ToastContainer } from '@manacore/shared-ui';
|
||||
import { AppLoadingSkeleton } from '$lib/components/skeletons';
|
||||
import { isLoading as i18nLoading } from 'svelte-i18n';
|
||||
import { onMount } from 'svelte';
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { toastStore } from '$lib/stores/toast.svelte';
|
||||
import type { Toast } from '$lib/stores/toast.svelte';
|
||||
import { X, CheckCircle, XCircle, Warning, Info } from '@manacore/shared-icons';
|
||||
|
||||
let toasts = $derived(toastStore.toasts);
|
||||
|
||||
const icons = {
|
||||
success: CheckCircle,
|
||||
error: XCircle,
|
||||
warning: Warning,
|
||||
info: Info,
|
||||
};
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-500/90 text-white',
|
||||
error: 'bg-destructive/90 text-destructive-foreground',
|
||||
warning: 'bg-amber-500/90 text-white',
|
||||
info: 'bg-primary/90 text-primary-foreground',
|
||||
};
|
||||
|
||||
function handleDismiss(id: string) {
|
||||
toastStore.dismiss(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if toasts.length > 0}
|
||||
<div class="fixed bottom-6 right-6 z-[100] flex flex-col gap-3 max-w-sm">
|
||||
{#each toasts as toast (toast.id)}
|
||||
{@const Icon = icons[toast.type]}
|
||||
<div
|
||||
class="flex items-start gap-3 px-4 py-3 rounded-xl shadow-lg backdrop-blur-xl border border-white/20 animate-slide-in {colors[
|
||||
toast.type
|
||||
]}"
|
||||
role="alert"
|
||||
>
|
||||
<Icon size={20} weight="fill" class="flex-shrink-0 mt-0.5" />
|
||||
<p class="flex-1 text-sm font-medium">{toast.message}</p>
|
||||
<button
|
||||
onclick={() => handleDismiss(toast.id)}
|
||||
class="flex-shrink-0 p-1 rounded-lg hover:bg-white/20 transition-colors"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slide-in 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<Toast[]>([]);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
</div>
|
||||
|
||||
<!-- Global Toast notifications -->
|
||||
<Toast />
|
||||
<ToastContainer />
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { toasts } from '$lib/stores/toast';
|
||||
import type { Toast } from '$lib/stores/toast';
|
||||
|
||||
function getIcon(type: Toast['type']) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return '✓';
|
||||
case 'error':
|
||||
return '✕';
|
||||
case 'warning':
|
||||
return '⚠';
|
||||
case 'info':
|
||||
default:
|
||||
return 'ℹ';
|
||||
}
|
||||
}
|
||||
|
||||
function getColorClass(type: Toast['type']) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return 'bg-green-500';
|
||||
case 'error':
|
||||
return 'bg-red-500';
|
||||
case 'warning':
|
||||
return 'bg-yellow-500';
|
||||
case 'info':
|
||||
default:
|
||||
return 'bg-blue-500';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
||||
{#each $toasts as toast (toast.id)}
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-lg bg-card px-4 py-3 shadow-lg border border-border animate-in slide-in-from-right duration-200"
|
||||
>
|
||||
<span
|
||||
class="{getColorClass(
|
||||
toast.type
|
||||
)} flex h-6 w-6 items-center justify-center rounded-full text-white text-sm"
|
||||
>
|
||||
{getIcon(toast.type)}
|
||||
</span>
|
||||
<span class="text-foreground">{toast.message}</span>
|
||||
<button
|
||||
onclick={() => toasts.remove(toast.id)}
|
||||
class="ml-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes slide-in-from-right {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: slide-in-from-right 0.2s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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<Toast[]>([]);
|
||||
|
||||
function addToast(toast: Omit<Toast, 'id'>) {
|
||||
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;
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { PageHeader } from '@manacore/shared-ui';
|
||||
import { PageHeader, toast } from '@manacore/shared-ui';
|
||||
import { alarmsStore } from '$lib/stores/alarms.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import type { CreateAlarmInput, Alarm } from '@clock/shared';
|
||||
import { ALARM_SOUNDS, DEFAULT_ALARM_PRESETS } from '@clock/shared';
|
||||
import { AlarmsSkeleton } from '$lib/components/skeletons';
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { browser } from '$app/environment';
|
||||
import { PageHeader } from '@manacore/shared-ui';
|
||||
import { PageHeader, toast } from '@manacore/shared-ui';
|
||||
import { timersStore } from '$lib/stores/timers.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { QUICK_TIMER_PRESETS, formatDuration } from '@clock/shared';
|
||||
import { TimersSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { PageHeader } from '@manacore/shared-ui';
|
||||
import { PageHeader, toast } from '@manacore/shared-ui';
|
||||
import { worldClocksStore } from '$lib/stores/world-clocks.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { POPULAR_TIMEZONES } from '@clock/shared';
|
||||
import WorldMap from '$lib/components/WorldMap.svelte';
|
||||
import { WorldClockSkeleton } from '$lib/components/skeletons';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { waitLocale } from '$lib/i18n';
|
||||
import ToastContainer from '$lib/components/ToastContainer.svelte';
|
||||
import { ToastContainer } from '@manacore/shared-ui';
|
||||
import { AppLoadingSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
let { children } = $props();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import ContactAlphabetView from '$lib/components/views/ContactAlphabetView.svelte';
|
||||
import { ContactListSkeleton, ContactGridSkeleton } from '$lib/components/skeletons';
|
||||
import { batchApi } from '$lib/api/batch';
|
||||
import { toasts } from '$lib/stores/toast';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte';
|
||||
|
||||
// Infinite scroll
|
||||
|
|
@ -183,12 +183,12 @@
|
|||
batchLoading = true;
|
||||
try {
|
||||
const result = await batchApi.deleteMany([...selectedIds]);
|
||||
toasts.success(`${result.success} Kontakte gelöscht`);
|
||||
toastStore.success(`${result.success} Kontakte gelöscht`);
|
||||
selectedIds = new Set();
|
||||
selectionMode = false;
|
||||
await contactsStore.loadContacts();
|
||||
} catch (e) {
|
||||
toasts.error(e instanceof Error ? e.message : 'Fehler beim Löschen');
|
||||
toastStore.error(e instanceof Error ? e.message : 'Fehler beim Löschen');
|
||||
} finally {
|
||||
batchLoading = false;
|
||||
}
|
||||
|
|
@ -200,12 +200,12 @@
|
|||
batchLoading = true;
|
||||
try {
|
||||
const result = await batchApi.archiveMany([...selectedIds], true);
|
||||
toasts.success(`${result.success} Kontakte archiviert`);
|
||||
toastStore.success(`${result.success} Kontakte archiviert`);
|
||||
selectedIds = new Set();
|
||||
selectionMode = false;
|
||||
await contactsStore.loadContacts();
|
||||
} catch (e) {
|
||||
toasts.error(e instanceof Error ? e.message : 'Fehler beim Archivieren');
|
||||
toastStore.error(e instanceof Error ? e.message : 'Fehler beim Archivieren');
|
||||
} finally {
|
||||
batchLoading = false;
|
||||
}
|
||||
|
|
@ -217,12 +217,12 @@
|
|||
batchLoading = true;
|
||||
try {
|
||||
const result = await batchApi.favoriteMany([...selectedIds], true);
|
||||
toasts.success(`${result.success} Kontakte zu Favoriten hinzugefügt`);
|
||||
toastStore.success(`${result.success} Kontakte zu Favoriten hinzugefügt`);
|
||||
selectedIds = new Set();
|
||||
selectionMode = false;
|
||||
await contactsStore.loadContacts();
|
||||
} catch (e) {
|
||||
toasts.error(e instanceof Error ? e.message : 'Fehler');
|
||||
toastStore.error(e instanceof Error ? e.message : 'Fehler');
|
||||
} finally {
|
||||
batchLoading = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { toasts } from '$lib/stores/toast';
|
||||
import type { Toast } from '$lib/stores/toast';
|
||||
|
||||
function getIcon(type: Toast['type']) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return '✓';
|
||||
case 'error':
|
||||
return '✕';
|
||||
case 'warning':
|
||||
return '⚠';
|
||||
case 'info':
|
||||
default:
|
||||
return 'ℹ';
|
||||
}
|
||||
}
|
||||
|
||||
function getColorClass(type: Toast['type']) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return 'bg-green-500';
|
||||
case 'error':
|
||||
return 'bg-red-500';
|
||||
case 'warning':
|
||||
return 'bg-yellow-500';
|
||||
case 'info':
|
||||
default:
|
||||
return 'bg-blue-500';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
||||
{#each $toasts as toast (toast.id)}
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-lg bg-card px-4 py-3 shadow-lg border border-border animate-in slide-in-from-right duration-200"
|
||||
>
|
||||
<span
|
||||
class="{getColorClass(
|
||||
toast.type
|
||||
)} flex h-6 w-6 items-center justify-center rounded-full text-white text-sm"
|
||||
>
|
||||
{getIcon(toast.type)}
|
||||
</span>
|
||||
<span class="text-foreground">{toast.message}</span>
|
||||
<button
|
||||
onclick={() => toasts.remove(toast.id)}
|
||||
class="ml-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes slide-in-from-right {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: slide-in-from-right 0.2s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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<Toast[]>([]);
|
||||
|
||||
function addToast(toast: Omit<Toast, 'id'>) {
|
||||
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();
|
||||
|
|
@ -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<DuplicateGroup[]>([]);
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
|
||||
import { toasts } from '$lib/stores/toast';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
|
||||
function handleSubscribe(planId: string) {
|
||||
console.log('Subscribe to plan:', planId);
|
||||
toasts.info(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);
|
||||
toastStore.info(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);
|
||||
}
|
||||
|
||||
function handleBuyPackage(packageId: string) {
|
||||
console.log('Buy package:', packageId);
|
||||
toasts.info(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`);
|
||||
toastStore.info(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { toasts, dismissToast } from '$lib/stores/toast';
|
||||
import type { Toast } from '$lib/stores/toast';
|
||||
import { fly, fade } from 'svelte/transition';
|
||||
import { CheckCircle, XCircle, Warning, Info, X } from '@manacore/shared-icons';
|
||||
|
||||
function getToastBgColor(type: Toast['type']) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return 'bg-green-50 border-green-200';
|
||||
case 'error':
|
||||
return 'bg-red-50 border-red-200';
|
||||
case 'warning':
|
||||
return 'bg-yellow-50 border-yellow-200';
|
||||
case 'info':
|
||||
default:
|
||||
return 'bg-blue-50 border-blue-200';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pointer-events-none fixed bottom-4 right-4 z-[100] flex flex-col gap-2">
|
||||
{#each $toasts as toast (toast.id)}
|
||||
{@const bgColor = getToastBgColor(toast.type)}
|
||||
<div
|
||||
transition:fly={{ y: 50, duration: 300 }}
|
||||
class="pointer-events-auto flex min-w-[320px] items-start gap-3 rounded-lg border p-4 shadow-lg {bgColor}"
|
||||
role="alert"
|
||||
>
|
||||
<span class="flex-shrink-0">
|
||||
{#if toast.type === 'success'}
|
||||
<CheckCircle size={24} weight="regular" class="text-green-500" />
|
||||
{:else if toast.type === 'error'}
|
||||
<XCircle size={24} weight="regular" class="text-red-500" />
|
||||
{:else if toast.type === 'warning'}
|
||||
<Warning size={24} weight="regular" class="text-yellow-500" />
|
||||
{:else}
|
||||
<Info size={24} weight="regular" class="text-blue-500" />
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<p class="flex-1 text-sm font-medium text-gray-900">{toast.message}</p>
|
||||
|
||||
<button
|
||||
onclick={() => dismissToast(toast.id)}
|
||||
class="flex-shrink-0 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<X size={20} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -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<Toast[]>([]);
|
||||
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 };
|
||||
|
|
@ -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<Toast[]>([]);
|
||||
|
||||
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([]);
|
||||
}
|
||||
|
|
@ -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?.()}
|
||||
|
||||
<!-- Global Toast Notifications -->
|
||||
<Toast />
|
||||
<ToastContainer />
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
|
||||
import { showToast } from '$lib/stores/toast';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
|
||||
function handleSubscribe(planId: string) {
|
||||
console.log('Subscribe to plan:', planId);
|
||||
showToast(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`, 'info', 5000);
|
||||
toastStore.show(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`, 'info', 5000);
|
||||
}
|
||||
|
||||
function handleBuyPackage(packageId: string) {
|
||||
console.log('Buy package:', packageId);
|
||||
showToast(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`, 'info', 5000);
|
||||
toastStore.show(
|
||||
`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`,
|
||||
'info',
|
||||
5000
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,188 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import type { Toast } from '$lib/stores/toast';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
let toasts = $state<Toast[]>([]);
|
||||
|
||||
toast.subscribe((value) => {
|
||||
toasts = value;
|
||||
});
|
||||
|
||||
function handleClose(id: string) {
|
||||
toast.remove(id);
|
||||
}
|
||||
|
||||
function getIcon(type: Toast['type']) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
</svg>`;
|
||||
case 'error':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||
</svg>`;
|
||||
case 'warning':
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>`;
|
||||
case 'info':
|
||||
default:
|
||||
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="toast-container">
|
||||
{#each toasts as toastItem (toastItem.id)}
|
||||
<div
|
||||
class="toast toast-{toastItem.type}"
|
||||
transition:fly={{ y: 20, duration: 300 }}
|
||||
role="alert"
|
||||
>
|
||||
<div class="toast-icon">
|
||||
{@html getIcon(toastItem.type)}
|
||||
</div>
|
||||
<p class="toast-message">{toastItem.message}</p>
|
||||
<button
|
||||
class="toast-close"
|
||||
onclick={() => handleClose(toastItem.id)}
|
||||
aria-label="Close notification"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
pointer-events: auto;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
border-left: 4px solid rgb(var(--color-success));
|
||||
}
|
||||
|
||||
.toast-success .toast-icon {
|
||||
color: rgb(var(--color-success));
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-left: 4px solid rgb(var(--color-error));
|
||||
}
|
||||
|
||||
.toast-error .toast-icon {
|
||||
color: rgb(var(--color-error));
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
border-left: 4px solid rgb(var(--color-warning));
|
||||
}
|
||||
|
||||
.toast-warning .toast-icon {
|
||||
color: rgb(var(--color-warning));
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-left: 4px solid rgb(var(--color-info));
|
||||
}
|
||||
|
||||
.toast-info .toast-icon {
|
||||
color: rgb(var(--color-info));
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--spacing-xs);
|
||||
cursor: pointer;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
transition: all var(--transition-fast);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
background: rgba(var(--color-border), 0.5);
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toast-container {
|
||||
bottom: 6rem;
|
||||
right: 1rem;
|
||||
left: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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<Toast[]>([]);
|
||||
|
||||
function add(toast: Omit<Toast, 'id'>) {
|
||||
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();
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { ChatCircle, PaperPlaneTilt } from '@manacore/shared-icons';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
|
||||
let type = $state<'bug' | 'feature' | 'other'>('feature');
|
||||
let message = $state('');
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
// Simulate sending feedback
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
toast.success('Feedback gesendet! Vielen Dank.');
|
||||
toastStore.success('Feedback gesendet! Vielen Dank.');
|
||||
message = '';
|
||||
type = 'feature';
|
||||
sending = false;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { GridFour, List, Plus, FolderPlus, UploadSimple } from '@manacore/shared-icons';
|
||||
import { filesStore } from '$lib/stores/files.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
import type { StorageFile, StorageFolder } from '$lib/api/client';
|
||||
import FileGrid from '$lib/components/files/FileGrid.svelte';
|
||||
import FileList from '$lib/components/files/FileList.svelte';
|
||||
|
|
@ -41,42 +41,42 @@
|
|||
switch (action) {
|
||||
case 'download':
|
||||
await filesStore.downloadFile(file.id, file.name);
|
||||
toast.success('Download gestartet');
|
||||
toastStore.success('Download gestartet');
|
||||
break;
|
||||
case 'rename':
|
||||
const newName = prompt('Neuer Name:', file.name);
|
||||
if (newName && newName !== file.name) {
|
||||
const result = await filesStore.renameFile(file.id, newName);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
toast.success('Datei umbenannt');
|
||||
toastStore.success('Datei umbenannt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'favorite':
|
||||
const favResult = await filesStore.toggleFileFavorite(file.id);
|
||||
if (!favResult.error) {
|
||||
toast.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
toastStore.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (confirm('Datei in den Papierkorb verschieben?')) {
|
||||
const delResult = await filesStore.deleteFile(file.id);
|
||||
if (delResult.error) {
|
||||
toast.error(delResult.error);
|
||||
toastStore.error(delResult.error);
|
||||
} else {
|
||||
toast.success('In den Papierkorb verschoben');
|
||||
toastStore.success('In den Papierkorb verschoben');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'share':
|
||||
// TODO: Open share modal
|
||||
toast.info('Teilen-Funktion kommt bald');
|
||||
toastStore.info('Teilen-Funktion kommt bald');
|
||||
break;
|
||||
case 'move':
|
||||
// TODO: Open move modal
|
||||
toast.info('Verschieben-Funktion kommt bald');
|
||||
toastStore.info('Verschieben-Funktion kommt bald');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -88,33 +88,33 @@
|
|||
if (newName && newName !== folder.name) {
|
||||
const result = await filesStore.renameFolder(folder.id, newName);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
toast.success('Ordner umbenannt');
|
||||
toastStore.success('Ordner umbenannt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'favorite':
|
||||
const favResult = await filesStore.toggleFolderFavorite(folder.id);
|
||||
if (!favResult.error) {
|
||||
toast.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
toastStore.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (confirm('Ordner und Inhalt in den Papierkorb verschieben?')) {
|
||||
const delResult = await filesStore.deleteFolder(folder.id);
|
||||
if (delResult.error) {
|
||||
toast.error(delResult.error);
|
||||
toastStore.error(delResult.error);
|
||||
} else {
|
||||
toast.success('In den Papierkorb verschoben');
|
||||
toastStore.success('In den Papierkorb verschoben');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'share':
|
||||
toast.info('Teilen-Funktion kommt bald');
|
||||
toastStore.info('Teilen-Funktion kommt bald');
|
||||
break;
|
||||
case 'move':
|
||||
toast.info('Verschieben-Funktion kommt bald');
|
||||
toastStore.info('Verschieben-Funktion kommt bald');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
for (const file of files) {
|
||||
const result = await filesStore.uploadFile(file);
|
||||
if (result.error) {
|
||||
toast.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
|
||||
toastStore.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
|
||||
}
|
||||
completed++;
|
||||
uploadProgress = Math.round((completed / totalFiles) * 100);
|
||||
|
|
@ -138,15 +138,15 @@
|
|||
uploading = false;
|
||||
uploadProgress = 0;
|
||||
showUploadZone = false;
|
||||
toast.success(`${totalFiles} Datei(en) hochgeladen`);
|
||||
toastStore.success(`${totalFiles} Datei(en) hochgeladen`);
|
||||
}
|
||||
|
||||
async function handleCreateFolder(name: string, color?: string) {
|
||||
const result = await filesStore.createFolder(name, color);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
toast.success('Ordner erstellt');
|
||||
toastStore.success('Ordner erstellt');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { GridFour, List, FolderPlus, UploadSimple, ArrowLeft } from '@manacore/shared-icons';
|
||||
import { filesStore } from '$lib/stores/files.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
import type { StorageFile, StorageFolder } from '$lib/api/client';
|
||||
import FileGrid from '$lib/components/files/FileGrid.svelte';
|
||||
import FileList from '$lib/components/files/FileList.svelte';
|
||||
|
|
@ -48,40 +48,40 @@
|
|||
switch (action) {
|
||||
case 'download':
|
||||
await filesStore.downloadFile(file.id, file.name);
|
||||
toast.success('Download gestartet');
|
||||
toastStore.success('Download gestartet');
|
||||
break;
|
||||
case 'rename':
|
||||
const newName = prompt('Neuer Name:', file.name);
|
||||
if (newName && newName !== file.name) {
|
||||
const result = await filesStore.renameFile(file.id, newName);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
toast.success('Datei umbenannt');
|
||||
toastStore.success('Datei umbenannt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'favorite':
|
||||
const favResult = await filesStore.toggleFileFavorite(file.id);
|
||||
if (!favResult.error) {
|
||||
toast.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
toastStore.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (confirm('Datei in den Papierkorb verschieben?')) {
|
||||
const delResult = await filesStore.deleteFile(file.id);
|
||||
if (delResult.error) {
|
||||
toast.error(delResult.error);
|
||||
toastStore.error(delResult.error);
|
||||
} else {
|
||||
toast.success('In den Papierkorb verschoben');
|
||||
toastStore.success('In den Papierkorb verschoben');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'share':
|
||||
toast.info('Teilen-Funktion kommt bald');
|
||||
toastStore.info('Teilen-Funktion kommt bald');
|
||||
break;
|
||||
case 'move':
|
||||
toast.info('Verschieben-Funktion kommt bald');
|
||||
toastStore.info('Verschieben-Funktion kommt bald');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -93,33 +93,33 @@
|
|||
if (newName && newName !== folder.name) {
|
||||
const result = await filesStore.renameFolder(folder.id, newName);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
toast.success('Ordner umbenannt');
|
||||
toastStore.success('Ordner umbenannt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'favorite':
|
||||
const favResult = await filesStore.toggleFolderFavorite(folder.id);
|
||||
if (!favResult.error) {
|
||||
toast.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
toastStore.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (confirm('Ordner und Inhalt in den Papierkorb verschieben?')) {
|
||||
const delResult = await filesStore.deleteFolder(folder.id);
|
||||
if (delResult.error) {
|
||||
toast.error(delResult.error);
|
||||
toastStore.error(delResult.error);
|
||||
} else {
|
||||
toast.success('In den Papierkorb verschoben');
|
||||
toastStore.success('In den Papierkorb verschoben');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'share':
|
||||
toast.info('Teilen-Funktion kommt bald');
|
||||
toastStore.info('Teilen-Funktion kommt bald');
|
||||
break;
|
||||
case 'move':
|
||||
toast.info('Verschieben-Funktion kommt bald');
|
||||
toastStore.info('Verschieben-Funktion kommt bald');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
for (const file of files) {
|
||||
const result = await filesStore.uploadFile(file);
|
||||
if (result.error) {
|
||||
toast.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
|
||||
toastStore.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
|
||||
}
|
||||
completed++;
|
||||
uploadProgress = Math.round((completed / totalFiles) * 100);
|
||||
|
|
@ -143,15 +143,15 @@
|
|||
uploading = false;
|
||||
uploadProgress = 0;
|
||||
showUploadZone = false;
|
||||
toast.success(`${totalFiles} Datei(en) hochgeladen`);
|
||||
toastStore.success(`${totalFiles} Datei(en) hochgeladen`);
|
||||
}
|
||||
|
||||
async function handleCreateFolder(name: string, color?: string) {
|
||||
const result = await filesStore.createFolder(name, color);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
toast.success('Ordner erstellt');
|
||||
toastStore.success('Ordner erstellt');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { ShareNetwork, Link, Copy, Trash } from '@manacore/shared-icons';
|
||||
import { sharesApi } from '$lib/api/client';
|
||||
import type { Share } from '$lib/api/client';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
|
||||
let shares = $state<Share[]>([]);
|
||||
let loading = $state(true);
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
async function copyShareLink(token: string) {
|
||||
const url = `${window.location.origin}/s/${token}`;
|
||||
await navigator.clipboard.writeText(url);
|
||||
toast.success('Link kopiert!');
|
||||
toastStore.success('Link kopiert!');
|
||||
}
|
||||
|
||||
async function deleteShare(id: string) {
|
||||
|
|
@ -38,10 +38,10 @@
|
|||
|
||||
const result = await sharesApi.delete(id);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
shares = shares.filter((s) => s.id !== id);
|
||||
toast.success('Share-Link gelöscht');
|
||||
toastStore.success('Share-Link gelöscht');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { Trash, ArrowCounterClockwise, Warning } from '@manacore/shared-icons';
|
||||
import { trashApi } from '$lib/api/client';
|
||||
import type { StorageFile, StorageFolder } from '$lib/api/client';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
|
||||
let files = $state<StorageFile[]>([]);
|
||||
let folders = $state<StorageFolder[]>([]);
|
||||
|
|
@ -32,14 +32,14 @@
|
|||
async function handleRestore(id: string, type: 'file' | 'folder') {
|
||||
const result = await trashApi.restore(id, type);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
if (type === 'file') {
|
||||
files = files.filter((f) => f.id !== id);
|
||||
} else {
|
||||
folders = folders.filter((f) => f.id !== id);
|
||||
}
|
||||
toast.success('Wiederhergestellt');
|
||||
toastStore.success('Wiederhergestellt');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,14 +48,14 @@
|
|||
|
||||
const result = await trashApi.permanentDelete(id, type);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
if (type === 'file') {
|
||||
files = files.filter((f) => f.id !== id);
|
||||
} else {
|
||||
folders = folders.filter((f) => f.id !== id);
|
||||
}
|
||||
toast.success('Endgültig gelöscht');
|
||||
toastStore.success('Endgültig gelöscht');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,11 +64,11 @@
|
|||
|
||||
const result = await trashApi.empty();
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
toastStore.error(result.error);
|
||||
} else {
|
||||
files = [];
|
||||
folders = [];
|
||||
toast.success('Papierkorb geleert');
|
||||
toastStore.success('Papierkorb geleert');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,3 +195,7 @@ export type {
|
|||
|
||||
// Immersive Mode
|
||||
export { default as ImmersiveModeToggle } from './components/ImmersiveModeToggle.svelte';
|
||||
|
||||
// Toast
|
||||
export { toastStore, toast, handleApiError, ToastContainer } from './toast';
|
||||
export type { Toast, ToastType } from './toast';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { Search, ZoomIn, ZoomOut, RotateCcw, Filter, X, Focus, Keyboard } from 'lucide-svelte';
|
||||
import {
|
||||
MagnifyingGlass,
|
||||
MagnifyingGlassPlus,
|
||||
MagnifyingGlassMinus,
|
||||
ArrowCounterClockwise,
|
||||
Funnel,
|
||||
X,
|
||||
Crosshair,
|
||||
Keyboard,
|
||||
} from '@manacore/shared-icons';
|
||||
import type { NetworkTag } from './network.types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -126,7 +135,7 @@
|
|||
<!-- Search bar -->
|
||||
{#if showSearch}
|
||||
<div class="search-container">
|
||||
<Search size={18} class="search-icon" />
|
||||
<MagnifyingGlass size={18} class="search-icon" />
|
||||
<input
|
||||
bind:this={searchInputElement}
|
||||
type="text"
|
||||
|
|
@ -152,7 +161,7 @@
|
|||
aria-label="Filter anzeigen"
|
||||
title="Filter"
|
||||
>
|
||||
<Filter size={18} />
|
||||
<Funnel size={18} />
|
||||
{#if hasActiveFilters}
|
||||
<span class="filter-badge"></span>
|
||||
{/if}
|
||||
|
|
@ -162,7 +171,7 @@
|
|||
<!-- Zoom controls -->
|
||||
<div class="zoom-controls">
|
||||
<button onclick={onZoomIn} class="control-btn" aria-label="Vergrößern" title="Vergrößern (+)">
|
||||
<ZoomIn size={18} />
|
||||
<MagnifyingGlassPlus size={18} />
|
||||
</button>
|
||||
<button
|
||||
onclick={onZoomOut}
|
||||
|
|
@ -170,7 +179,7 @@
|
|||
aria-label="Verkleinern"
|
||||
title="Verkleinern (-)"
|
||||
>
|
||||
<ZoomOut size={18} />
|
||||
<MagnifyingGlassMinus size={18} />
|
||||
</button>
|
||||
<button
|
||||
onclick={onResetZoom}
|
||||
|
|
@ -178,7 +187,7 @@
|
|||
aria-label="Ansicht zurücksetzen"
|
||||
title="Zurücksetzen (0)"
|
||||
>
|
||||
<RotateCcw size={18} />
|
||||
<ArrowCounterClockwise size={18} />
|
||||
</button>
|
||||
<button
|
||||
onclick={onFocusSelected}
|
||||
|
|
@ -186,7 +195,7 @@
|
|||
aria-label="Auf Auswahl fokussieren"
|
||||
title="Fokus auf Auswahl (F)"
|
||||
>
|
||||
<Focus size={18} />
|
||||
<Crosshair size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
139
packages/shared-ui/src/toast/ToastContainer.svelte
Normal file
139
packages/shared-ui/src/toast/ToastContainer.svelte
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<script lang="ts">
|
||||
import { toastStore } from './toast.svelte';
|
||||
import type { Toast } from './toast.svelte';
|
||||
import { X, CheckCircle, XCircle, Warning, Info } from '@manacore/shared-icons';
|
||||
|
||||
let toasts = $derived(toastStore.toasts);
|
||||
|
||||
const icons = {
|
||||
success: CheckCircle,
|
||||
error: XCircle,
|
||||
warning: Warning,
|
||||
info: Info,
|
||||
};
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-500/90 text-white',
|
||||
error: 'bg-destructive/90 text-destructive-foreground',
|
||||
warning: 'bg-amber-500/90 text-white',
|
||||
info: 'bg-primary/90 text-primary-foreground',
|
||||
};
|
||||
|
||||
function handleDismiss(id: string) {
|
||||
toastStore.dismiss(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if toasts.length > 0}
|
||||
<div class="toast-container">
|
||||
{#each toasts as toast (toast.id)}
|
||||
{@const Icon = icons[toast.type]}
|
||||
<div class="toast-item {colors[toast.type]}" role="alert">
|
||||
<Icon size={20} weight="fill" class="toast-icon" />
|
||||
<p class="toast-message">{toast.message}</p>
|
||||
<button
|
||||
onclick={() => handleDismiss(toast.id)}
|
||||
class="toast-dismiss"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
/* Mobile: full width at bottom */
|
||||
@media (max-width: 640px) {
|
||||
.toast-container {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
animation: toast-slide-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.toast-dismiss {
|
||||
flex-shrink: 0;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.toast-dismiss:hover {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@keyframes toast-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: slide up instead of from right */
|
||||
@media (max-width: 640px) {
|
||||
@keyframes toast-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
packages/shared-ui/src/toast/index.ts
Normal file
3
packages/shared-ui/src/toast/index.ts
Normal file
|
|
@ -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';
|
||||
146
packages/shared-ui/src/toast/toast.svelte.ts
Normal file
146
packages/shared-ui/src/toast/toast.svelte.ts
Normal file
|
|
@ -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<Toast[]>([]);
|
||||
|
||||
// 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue