feat: add toast notification system, replace all alert() calls
Create shared toast store and Toast component mounted in root layout. Wire toast.error() into all fitness API calls that previously failed silently, and replace all alert() calls across recipes and cospend.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<script>
|
||||
import { X } from 'lucide-svelte';
|
||||
import { getToasts } from '$lib/js/toast.svelte';
|
||||
|
||||
const toasts = getToasts();
|
||||
</script>
|
||||
|
||||
{#if toasts.items.length > 0}
|
||||
<div class="toast-container">
|
||||
{#each toasts.items as t (t.id)}
|
||||
<div class="toast toast-{t.type}" role="alert">
|
||||
<span class="toast-msg">{t.message}</span>
|
||||
<button class="toast-close" onclick={() => toasts.remove(t.id)} aria-label="Dismiss">
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - 2rem);
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
pointer-events: auto;
|
||||
animation: slide-up 0.2s ease-out;
|
||||
}
|
||||
.toast-error {
|
||||
background: var(--nord11);
|
||||
color: var(--nord6, #eceff4);
|
||||
}
|
||||
.toast-success {
|
||||
background: var(--nord14);
|
||||
color: var(--nord0, #2e3440);
|
||||
}
|
||||
.toast-info {
|
||||
background: var(--nord10);
|
||||
color: var(--nord6, #eceff4);
|
||||
}
|
||||
.toast-msg {
|
||||
flex: 1;
|
||||
}
|
||||
.toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
}
|
||||
.toast-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@keyframes slide-up {
|
||||
from { opacity: 0; transform: translateY(0.5rem); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
|
||||
import Cross from '$lib/assets/icons/Cross.svelte'
|
||||
import { toast } from '$lib/js/toast.svelte'
|
||||
import "$lib/css/shake.css"
|
||||
import "$lib/css/icon.css"
|
||||
import { onMount } from 'svelte'
|
||||
@@ -32,14 +33,14 @@ function handleFileSelect(event: Event) {
|
||||
|
||||
// Validate MIME type
|
||||
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
|
||||
alert('Invalid file type. Please upload a JPEG, PNG, or WebP image.');
|
||||
toast.error('Invalid file type. Please upload a JPEG, PNG, or WebP image.');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
alert(`File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`);
|
||||
toast.error(`File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`);
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
interface Toast {
|
||||
id: number;
|
||||
message: string;
|
||||
type: 'error' | 'success' | 'info';
|
||||
}
|
||||
|
||||
let nextId = 0;
|
||||
let toasts = $state<Toast[]>([]);
|
||||
|
||||
export function getToasts() {
|
||||
return {
|
||||
get items() { return toasts; },
|
||||
remove(id: number) {
|
||||
toasts = toasts.filter(t => t.id !== id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function add(message: string, type: Toast['type'], duration = 5000) {
|
||||
const id = nextId++;
|
||||
toasts = [...toasts, { id, message, type }];
|
||||
setTimeout(() => {
|
||||
toasts = toasts.filter(t => t.id !== id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
export const toast = {
|
||||
error: (msg: string) => add(msg, 'error', 6000),
|
||||
success: (msg: string) => add(msg, 'success', 3000),
|
||||
info: (msg: string) => add(msg, 'info', 4000),
|
||||
};
|
||||
Reference in New Issue
Block a user