Better error norifications

This commit is contained in:
Roman Krček
2025-07-14 14:30:55 +02:00
parent a8f1b973e6
commit 06f2553b42
14 changed files with 513 additions and 68 deletions

View File

@@ -1,11 +1,127 @@
<script lang="ts">
let { error } = $props<{
error: string;
import { onMount } from 'svelte';
let {
message,
type = 'error',
duration = 50000,
onDismiss
} = $props<{
message: string;
type?: 'error' | 'success' | 'warning' | 'info';
duration?: number;
onDismiss?: () => void;
}>();
let visible = $state(true);
let timeoutId: ReturnType<typeof setTimeout>;
// Auto-dismiss after specified duration
onMount(() => {
if (duration > 0) {
timeoutId = setTimeout(() => {
dismiss();
}, duration);
}
// Cleanup timeout on component destroy
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
});
function dismiss() {
visible = false;
if (onDismiss) {
onDismiss();
}
}
// Get styles based on toast type
const getToastStyles = (type: string) => {
switch (type) {
case 'success':
return 'border-green-200 bg-green-50 text-green-800';
case 'warning':
return 'border-yellow-200 bg-yellow-50 text-yellow-800';
case 'info':
return 'border-blue-200 bg-blue-50 text-blue-800';
case 'error':
default:
return 'border-red-200 bg-red-50 text-red-800';
}
};
// Get icon based on toast type
const getIcon = (type: string) => {
switch (type) {
case 'success':
return '✓';
case 'warning':
return '⚠';
case 'info':
return '';
case 'error':
default:
return '✕';
}
};
</script>
{#if error}
<div class="mt-4 rounded border border-red-200 bg-red-50 p-3">
<p class="text-sm text-red-600">{error}</p>
{#if visible && message}
<div
class="fixed top-4 left-4 z-50 max-w-sm rounded-lg border p-4 shadow-lg transition-all duration-300 ease-in-out {getToastStyles(type)}"
role="alert"
aria-live="polite"
>
<div class="flex items-start gap-3">
<!-- Icon -->
<div class="flex-shrink-0">
<span class="text-lg font-semibold" aria-hidden="true">
{getIcon(type)}
</span>
</div>
<!-- Message -->
<div class="flex-1">
<p class="text-sm font-medium">
{message}
</p>
</div>
<!-- Close button -->
<button
onclick={dismiss}
class="flex-shrink-0 ml-2 text-current opacity-70 hover:opacity-100 transition-opacity"
aria-label="Dismiss notification"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Progress bar for auto-dismiss -->
{#if duration > 0}
<div class="mt-2 h-1 w-full bg-black bg-opacity-10 rounded-full overflow-hidden">
<div
class="h-full bg-current opacity-30 transition-all ease-linear"
style="animation: progress {duration}ms linear forwards;"
></div>
</div>
{/if}
</div>
{/if}
<style>
@keyframes progress {
from {
width: 100%;
}
to {
width: 0%;
}
}
</style>

View File

@@ -6,10 +6,9 @@
sheet_id: string;
}
let { event, loading, error } = $props<{
let { event, loading } = $props<{
event: Event | null;
loading: boolean;
error: string;
}>();
function formatDate(dateString: string) {
@@ -80,9 +79,9 @@
</div>
</div>
</div>
{:else if error}
{:else}
<div class="py-8 text-center">
<p class="text-red-600">{error}</p>
<p class="text-gray-600">No event information available</p>
</div>
{/if}
</div>

View File

@@ -0,0 +1,105 @@
<script lang="ts">
import { onMount } from 'svelte';
let {
message,
type = 'error',
duration = 5000,
onDismiss
} = $props<{
message: string;
type?: 'error' | 'success' | 'warning' | 'info';
duration?: number;
onDismiss?: () => void;
}>();
let visible = $state(true);
let timeoutId: ReturnType<typeof setTimeout>;
// Auto-dismiss after specified duration
onMount(() => {
if (duration > 0) {
timeoutId = setTimeout(() => {
dismiss();
}, duration);
}
// Cleanup timeout on component destroy
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
});
function dismiss() {
visible = false;
if (onDismiss) {
onDismiss();
}
}
// Get styles based on toast type
const getToastStyles = (type: string) => {
const baseStyles = "fixed top-4 left-4 z-50 p-4 rounded-lg shadow-lg border max-w-sm";
switch (type) {
case 'success':
return `${baseStyles} bg-green-50 border-green-200 text-green-800`;
case 'warning':
return `${baseStyles} bg-yellow-50 border-yellow-200 text-yellow-800`;
case 'info':
return `${baseStyles} bg-blue-50 border-blue-200 text-blue-800`;
case 'error':
default:
return `${baseStyles} bg-red-50 border-red-200 text-red-800`;
}
};
const getIconSvg = (type: string) => {
switch (type) {
case 'success':
return `<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />`;
case 'warning':
return `<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16c-.77.833.192 2.5 1.732 2.5z" />`;
case 'info':
return `<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />`;
case 'error':
default:
return `<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />`;
}
};
</script>
{#if visible}
<div class={getToastStyles(type)} role="alert">
<div class="flex items-start gap-3">
<!-- Icon -->
<svg
class="h-5 w-5 flex-shrink-0 mt-0.5"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
{@html getIconSvg(type)}
</svg>
<!-- Message -->
<div class="flex-1">
<p class="text-sm font-medium">{message}</p>
</div>
<!-- Close button -->
<button
onclick={dismiss}
class="flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Dismiss notification"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/if}