diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 229b045..a7768ce 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,6 +9,8 @@ Basics: These you need to really follow! - Never use supabse-js. I am using supabse-ssr and supabase client is located in: - client: $props.data.supabse - server: $locals.supabase +- Avoid unnceessary iterations. Once the problem is solved, ask me if i want to to continue and only then continue iterating. +- Avoid sweeping changes throught the project. If you want to change something globally, ask me first. Do not fall back to the legacy $: label syntax or Svelte 3/4 stores! This is important! diff --git a/src/lib/components/ToastContainer.svelte b/src/lib/components/ToastContainer.svelte new file mode 100644 index 0000000..7238377 --- /dev/null +++ b/src/lib/components/ToastContainer.svelte @@ -0,0 +1,25 @@ + + + +
+ {#each toasts as toastItem (toastItem.id)} +
+ handleDismiss(toastItem.id)} + /> +
+ {/each} +
diff --git a/src/lib/components/ToastNotification.svelte b/src/lib/components/ToastNotification.svelte new file mode 100644 index 0000000..8ca8d8a --- /dev/null +++ b/src/lib/components/ToastNotification.svelte @@ -0,0 +1,115 @@ + + +{#if visible && message} + +{/if} diff --git a/src/lib/stores/toast.ts b/src/lib/stores/toast.ts new file mode 100644 index 0000000..2ecea6b --- /dev/null +++ b/src/lib/stores/toast.ts @@ -0,0 +1,63 @@ +import { writable } from 'svelte/store'; + +export interface Toast { + id: string; + message: string; + type: 'error' | 'success' | 'warning' | 'info'; + duration?: number; +} + +function createToastStore() { + const { subscribe, update } = writable([]); + + const store = { + subscribe, + + // Add a new toast + add: (toast: Omit) => { + const id = crypto.randomUUID(); + const newToast: Toast = { + id, + duration: 5000, // Default 5 seconds + ...toast + }; + + update(toasts => [...toasts, newToast]); + return id; + }, + + // Remove a toast by ID + remove: (id: string) => { + update(toasts => toasts.filter(toast => toast.id !== id)); + }, + + // Clear all toasts + clear: () => { + update(() => []); + } + }; + + // Add convenience methods that reference the same store instance + return { + ...store, + + // Convenience methods for different toast types + success: (message: string, duration?: number) => { + return store.add({ message, type: 'success', duration }); + }, + + error: (message: string, duration?: number) => { + return store.add({ message, type: 'error', duration }); + }, + + warning: (message: string, duration?: number) => { + return store.add({ message, type: 'warning', duration }); + }, + + info: (message: string, duration?: number) => { + return store.add({ message, type: 'info', duration }); + } + }; +} + +export const toast = createToastStore(); diff --git a/src/routes/private/+layout.svelte b/src/routes/private/+layout.svelte index 2f63c7b..f45a903 100644 --- a/src/routes/private/+layout.svelte +++ b/src/routes/private/+layout.svelte @@ -1,6 +1,7 @@ @@ -22,9 +21,6 @@ class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="Enter event name" /> - {#if errors.name} -

{errors.name}

- {/if}
@@ -37,8 +33,5 @@ bind:value={eventData.date} class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> - {#if errors.date} -

{errors.date}

- {/if}
diff --git a/src/routes/private/events/event/new/components/GoogleAuthStep.svelte b/src/routes/private/events/event/new/components/GoogleAuthStep.svelte index 05199b8..ec72a5e 100644 --- a/src/routes/private/events/event/new/components/GoogleAuthStep.svelte +++ b/src/routes/private/events/event/new/components/GoogleAuthStep.svelte @@ -2,8 +2,7 @@ import GoogleAuthButton from '$lib/components/GoogleAuthButton.svelte'; // Props - let { errors, onSuccess, onError } = $props<{ - errors: Record; + let { onSuccess, onError } = $props<{ onSuccess?: (token: string) => void; onError?: (error: string) => void; }>(); @@ -23,10 +22,6 @@ onError={onError} /> - {#if errors.google} -
- {errors.google} -
- {/if} + diff --git a/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte b/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte index 799fc0a..ed9451a 100644 --- a/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte +++ b/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte @@ -2,7 +2,7 @@ import type { GoogleSheet } from '$lib/google/sheets/types.ts'; // Props - let { sheetsData = $bindable(), errors = $bindable(), loadRecentSheets, selectSheet, toggleSheetList } = $props<{ + let { sheetsData = $bindable(), loadRecentSheets, selectSheet, toggleSheetList } = $props<{ sheetsData: { availableSheets: GoogleSheet[]; selectedSheet: GoogleSheet | null; @@ -16,7 +16,6 @@ loading: boolean; expandedSheetList: boolean; }; - errors: Record; loadRecentSheets: () => Promise; selectSheet: (sheet: GoogleSheet) => Promise; toggleSheetList: () => void; @@ -257,9 +256,7 @@ {/if} - {#if errors.sheet} -

{errors.sheet}

- {/if} + {#if sheetsData.selectedSheet && sheetsData.sheetData.length > 0} @@ -374,9 +371,7 @@ {/if} - {#if errors.sheetData} -

{errors.sheetData}

- {/if} + diff --git a/src/routes/private/events/event/view/+page.svelte b/src/routes/private/events/event/view/+page.svelte index 5a36662..6daffb4 100644 --- a/src/routes/private/events/event/view/+page.svelte +++ b/src/routes/private/events/event/view/+page.svelte @@ -9,8 +9,9 @@ import ParticipantsTable from './components/ParticipantsTable.svelte'; import EmailSending from './components/EmailSending.svelte'; import EmailResults from './components/EmailResults.svelte'; - import ErrorMessage from './components/ErrorMessage.svelte'; import Statistics from './components/Statistics.svelte'; + import ToastContainer from '$lib/components/ToastContainer.svelte'; + import { toast } from '$lib/stores/toast.js'; let { data } = $props(); @@ -49,7 +50,6 @@ let sendingEmails = $state(false); let emailProgress = $state({ sent: 0, total: 0 }); let emailResults = $state<{success: boolean, results: any[], summary: any} | null>(null); - let error = $state(''); // Get event ID from URL params let eventId = $derived(page.url.searchParams.get('id')); @@ -74,7 +74,10 @@ event = eventData; } catch (err) { console.error('Error loading event:', err); - error = 'Failed to load event'; + toast.add({ + message: 'Failed to load event', + type: 'error' + }); } finally { loading = false; } @@ -95,7 +98,10 @@ participants = participantsData || []; } catch (err) { console.error('Error loading participants:', err); - error = 'Failed to load participants'; + toast.add({ + message: 'Failed to load participants', + type: 'error' + }); } finally { participantsLoading = false; } @@ -107,12 +113,14 @@ // Check if user has Google authentication const refreshToken = localStorage.getItem('google_refresh_token'); if (!refreshToken) { - error = 'Please connect your Google account first to sync participants'; + toast.add({ + message: 'Please connect your Google account first to sync participants', + type: 'error' + }); return; } syncingParticipants = true; - error = ''; try { // Fetch sheet data const response = await fetch(`/private/api/google/sheets/${event.sheet_id}/data`, { @@ -177,7 +185,10 @@ await loadParticipants(); } catch (err) { console.error('Error syncing participants:', err); - error = 'Failed to sync participants'; + toast.add({ + message: 'Failed to sync participants', + type: 'error' + }); } finally { syncingParticipants = false; } @@ -189,20 +200,25 @@ // Check if user has Google authentication const refreshToken = localStorage.getItem('google_refresh_token'); if (!refreshToken) { - error = 'Please connect your Google account first to send emails'; + toast.add({ + message: 'Please connect your Google account first to send emails', + type: 'error' + }); return; } const uncontactedParticipants = participants.filter(p => !p.email_sent); if (uncontactedParticipants.length === 0) { - error = 'No uncontacted participants found'; + toast.add({ + message: 'No uncontacted participants found', + type: 'warning' + }); return; } sendingEmails = true; emailProgress = { sent: 0, total: uncontactedParticipants.length }; emailResults = null; - error = ''; try { // Send all emails in batch @@ -235,33 +251,40 @@ }); } else { const errorData = await response.json(); - error = errorData.error || 'Failed to send emails'; + toast.add({ + message: errorData.error || 'Failed to send emails', + type: 'error' + }); console.error('Email sending failed:', errorData); } } catch (err) { console.error('Error sending emails:', err); - error = 'Failed to send emails to participants'; + toast.add({ + message: 'Failed to send emails to participants', + type: 'error' + }); } finally { sendingEmails = false; } } function handleGoogleAuthSuccess() { - error = ''; + // Success handled by toast in the component } function handleGoogleAuthError(errorMsg: string) { - error = errorMsg; + toast.add({ + message: errorMsg, + type: 'error' + }); } -

Event Overview

- - + {/if} - -{#if error} - -{/if} diff --git a/src/routes/private/events/event/view/components/ErrorMessage.svelte b/src/routes/private/events/event/view/components/ErrorMessage.svelte index e222576..94b53f4 100644 --- a/src/routes/private/events/event/view/components/ErrorMessage.svelte +++ b/src/routes/private/events/event/view/components/ErrorMessage.svelte @@ -1,11 +1,127 @@ -{#if error} -
-

{error}

+{#if visible && message} + {/if} + + diff --git a/src/routes/private/events/event/view/components/EventInformation.svelte b/src/routes/private/events/event/view/components/EventInformation.svelte index 7d99aac..ccce29c 100644 --- a/src/routes/private/events/event/view/components/EventInformation.svelte +++ b/src/routes/private/events/event/view/components/EventInformation.svelte @@ -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 @@
- {:else if error} + {:else}
-

{error}

+

No event information available

{/if} diff --git a/src/routes/private/events/event/view/components/ToastNotification.svelte b/src/routes/private/events/event/view/components/ToastNotification.svelte new file mode 100644 index 0000000..202c588 --- /dev/null +++ b/src/routes/private/events/event/view/components/ToastNotification.svelte @@ -0,0 +1,105 @@ + + +{#if visible} + +{/if}