mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(chat): add toast notification system and docker AI config
- Add global Toast component with success/error/warning/info types - Create toastStore using Svelte 5 runes for centralized notifications - Add GOOGLE_GENAI_API_KEY to mana-core-auth docker-compose config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4f06301fe3
commit
0893ed7daa
5 changed files with 175 additions and 2 deletions
63
apps/chat/apps/web/src/lib/components/Toast.svelte
Normal file
63
apps/chat/apps/web/src/lib/components/Toast.svelte
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { toastStore, 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>
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { conversationService } from '$lib/services/conversation';
|
import { conversationService } from '$lib/services/conversation';
|
||||||
|
import { toastStore } from './toast.svelte';
|
||||||
import type { Conversation } from '@chat/types';
|
import type { Conversation } from '@chat/types';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
@ -36,7 +37,9 @@ export const conversationsStore = {
|
||||||
try {
|
try {
|
||||||
conversations = await conversationService.getConversations(spaceId);
|
conversations = await conversationService.getConversations(spaceId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Failed to load conversations';
|
const message = e instanceof Error ? e.message : 'Konversationen konnten nicht geladen werden';
|
||||||
|
error = message;
|
||||||
|
toastStore.error(message);
|
||||||
conversations = [];
|
conversations = [];
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
@ -53,7 +56,9 @@ export const conversationsStore = {
|
||||||
try {
|
try {
|
||||||
archivedConversations = await conversationService.getArchivedConversations();
|
archivedConversations = await conversationService.getArchivedConversations();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Failed to load archived conversations';
|
const message = e instanceof Error ? e.message : 'Archivierte Konversationen konnten nicht geladen werden';
|
||||||
|
error = message;
|
||||||
|
toastStore.error(message);
|
||||||
archivedConversations = [];
|
archivedConversations = [];
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
|
||||||
100
apps/chat/apps/web/src/lib/stores/toast.svelte.ts
Normal file
100
apps/chat/apps/web/src/lib/stores/toast.svelte.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { theme } from '$lib/stores/theme';
|
import { theme } from '$lib/stores/theme';
|
||||||
|
import Toast from '$lib/components/Toast.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
|
@ -14,3 +15,6 @@
|
||||||
<div class="min-h-screen bg-background text-foreground">
|
<div class="min-h-screen bg-background text-foreground">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Global Toast notifications -->
|
||||||
|
<Toast />
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ services:
|
||||||
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173,http://localhost:8081}
|
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173,http://localhost:8081}
|
||||||
CREDITS_SIGNUP_BONUS: ${CREDITS_SIGNUP_BONUS:-150}
|
CREDITS_SIGNUP_BONUS: ${CREDITS_SIGNUP_BONUS:-150}
|
||||||
CREDITS_DAILY_FREE: ${CREDITS_DAILY_FREE:-5}
|
CREDITS_DAILY_FREE: ${CREDITS_DAILY_FREE:-5}
|
||||||
|
GOOGLE_GENAI_API_KEY: ${GOOGLE_GENAI_API_KEY}
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue