From 06f2553b4200d9157cdb9462dff422a46fc4dc32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Kr=C4=8Dek?= Date: Mon, 14 Jul 2025 14:30:55 +0200 Subject: [PATCH 1/8] Better error norifications --- .github/copilot-instructions.md | 2 + src/lib/components/ToastContainer.svelte | 25 ++++ src/lib/components/ToastNotification.svelte | 115 ++++++++++++++++ src/lib/stores/toast.ts | 63 +++++++++ src/routes/private/+layout.svelte | 15 ++- .../private/events/event/new/+page.svelte | 23 +++- .../new/components/EmailSettingsStep.svelte | 10 +- .../new/components/EventDetailsStep.svelte | 9 +- .../new/components/GoogleAuthStep.svelte | 9 +- .../new/components/GoogleSheetsStep.svelte | 11 +- .../private/events/event/view/+page.svelte | 61 ++++++--- .../event/view/components/ErrorMessage.svelte | 126 +++++++++++++++++- .../view/components/EventInformation.svelte | 7 +- .../view/components/ToastNotification.svelte | 105 +++++++++++++++ 14 files changed, 513 insertions(+), 68 deletions(-) create mode 100644 src/lib/components/ToastContainer.svelte create mode 100644 src/lib/components/ToastNotification.svelte create mode 100644 src/lib/stores/toast.ts create mode 100644 src/routes/private/events/event/view/components/ToastNotification.svelte 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} From b9db3d22a353572c00fbca806e20a3a3dea8478a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Kr=C4=8Dek?= Date: Mon, 14 Jul 2025 14:34:38 +0200 Subject: [PATCH 2/8] Cleanup for error notifications --- .../private/events/event/new/+page.svelte | 156 +++++++++--------- .../new/components/EmailSettingsStep.svelte | 1 - .../new/components/GoogleAuthStep.svelte | 2 - .../new/components/GoogleSheetsStep.svelte | 4 - 4 files changed, 76 insertions(+), 87 deletions(-) diff --git a/src/routes/private/events/event/new/+page.svelte b/src/routes/private/events/event/new/+page.svelte index 31a174e..41739bf 100644 --- a/src/routes/private/events/event/new/+page.svelte +++ b/src/routes/private/events/event/new/+page.svelte @@ -4,7 +4,7 @@ import type { GoogleSheet } from '$lib/google/sheets/types.ts'; import { goto } from '$app/navigation'; import { toast } from '$lib/stores/toast.js'; - + // Import Components import GoogleAuthStep from './components/GoogleAuthStep.svelte'; import EventDetailsStep from './components/EventDetailsStep.svelte'; @@ -42,7 +42,7 @@ selectedSheet: null as GoogleSheet | null, sheetData: [] as string[][], columnMapping: { - name: 0, // Initialize to 0 (no column selected) + name: 0, // Initialize to 0 (no column selected) surname: 0, email: 0, confirmation: 0 @@ -64,7 +64,7 @@ onMount(async () => { // Check Google auth status on mount await checkGoogleAuth(); - + if (currentStep === 2) { await loadRecentSheets(); } @@ -76,13 +76,13 @@ try { const accessToken = localStorage.getItem('google_access_token'); const refreshToken = localStorage.getItem('google_refresh_token'); - + if (accessToken && refreshToken) { // Check if token is still valid const isValid = await isTokenValid(accessToken); authData.isConnected = isValid; authData.token = accessToken; - + if (isValid) { // Fetch user info await fetchUserInfo(accessToken); @@ -104,15 +104,16 @@ async function connectToGoogle() { authData.error = ''; authData.connecting = true; - + try { // Open popup window for OAuth const popup = window.open( '/auth/google', 'google-auth', - 'width=500,height=600,scrollbars=yes,resizable=yes,left=' + - Math.round(window.screen.width / 2 - 250) + ',top=' + - Math.round(window.screen.height / 2 - 300) + 'width=500,height=600,scrollbars=yes,resizable=yes,left=' + + Math.round(window.screen.width / 2 - 250) + + ',top=' + + Math.round(window.screen.height / 2 - 300) ); if (!popup) { @@ -127,12 +128,12 @@ // Store current timestamp to detect changes in localStorage const startTimestamp = localStorage.getItem('google_auth_timestamp') || '0'; - + // Poll localStorage for auth completion const pollInterval = setInterval(() => { try { const currentTimestamp = localStorage.getItem('google_auth_timestamp'); - + // If timestamp has changed, auth is complete if (currentTimestamp && currentTimestamp !== startTimestamp) { handleAuthSuccess(); @@ -141,24 +142,24 @@ console.error('Error checking auth timestamp:', e); } }, 500); // Poll every 500ms - + // Common handler for authentication success function handleAuthSuccess() { if (authCompleted) return; // Prevent duplicate handling - + authCompleted = true; authData.connecting = false; authData.showCancelOption = false; - + // Clean up timers clearInterval(pollInterval); if (popupTimer) clearTimeout(popupTimer); if (cancelTimeout) clearTimeout(cancelTimeout); - + // Update auth state setTimeout(checkGoogleAuth, 100); } - + // Clean up function to handle all cleanup in one place const cleanUp = () => { clearInterval(pollInterval); @@ -190,19 +191,18 @@ cleanUp(); } }, 60 * 1000); // Reduced from 3min to 1min - } catch (error) { console.error('Error connecting to Google:', error); authData.error = 'Failed to connect to Google'; authData.connecting = false; } } - + function cancelGoogleAuth() { authData.connecting = false; authData.showCancelOption = false; } - + async function fetchUserInfo(accessToken: string) { try { // Use the new getUserInfo function from our lib @@ -217,7 +217,7 @@ authData.userEmail = null; } } - + async function disconnectGoogle() { try { // First revoke the token at Google using our API @@ -225,16 +225,16 @@ if (accessToken) { await revokeToken(accessToken); } - + // Remove tokens from local storage localStorage.removeItem('google_access_token'); localStorage.removeItem('google_refresh_token'); - + // Update auth state authData.isConnected = false; authData.token = null; authData.userEmail = null; - + // Clear any selected sheets data sheetsData.availableSheets = []; sheetsData.selectedSheet = null; @@ -263,7 +263,7 @@ // Clear previous errors errors = {}; let isValid = true; - + if (currentStep === 0) { if (!authData.isConnected) { toast.error('Please connect your Google account to continue'); @@ -287,17 +287,17 @@ errors.sheet = 'Please select a Google Sheet'; isValid = false; } - + if (sheetsData.selectedSheet) { // Validate column mappings const { name, surname, email, confirmation } = sheetsData.columnMapping; const missingColumns = []; - + if (!name) missingColumns.push('Name'); if (!surname) missingColumns.push('Surname'); if (!email) missingColumns.push('Email'); if (!confirmation) missingColumns.push('Confirmation'); - + if (missingColumns.length > 0) { const errorMsg = `Please map the following columns: ${missingColumns.join(', ')}`; toast.error(errorMsg); @@ -326,16 +326,16 @@ sheetsData.loading = true; // Always expand the sheet list when loading new sheets sheetsData.expandedSheetList = true; - + try { // Use the new unified API endpoint const response = await fetch('/private/api/google/sheets/recent', { method: 'GET', headers: { - 'Authorization': `Bearer ${localStorage.getItem('google_refresh_token')}` + Authorization: `Bearer ${localStorage.getItem('google_refresh_token')}` } }); - + if (response.ok) { sheetsData.availableSheets = await response.json(); } @@ -349,24 +349,24 @@ async function selectSheet(sheet: GoogleSheet) { const sameSheet = sheetsData.selectedSheet?.id === sheet.id; - + sheetsData.selectedSheet = sheet; sheetsData.loading = true; - + // Collapse sheet list when selecting a new sheet if (!sameSheet) { sheetsData.expandedSheetList = false; } - + try { // Use the new unified API endpoint const response = await fetch(`/private/api/google/sheets/${sheet.id}/data`, { method: 'GET', headers: { - 'Authorization': `Bearer ${localStorage.getItem('google_refresh_token')}` + Authorization: `Bearer ${localStorage.getItem('google_refresh_token')}` } }); - + if (response.ok) { const data = await response.json(); sheetsData.sheetData = data.values || []; @@ -378,7 +378,7 @@ sheetsData.loading = false; } } - + // Toggle the sheet list expansion function toggleSheetList() { sheetsData.expandedSheetList = !sheetsData.expandedSheetList; @@ -427,49 +427,45 @@ }); -
- - + + - -
- {#if currentStep === 0} - { - authData.error = null; - authData.token = token; - authData.isConnected = true; - setTimeout(checkGoogleAuth, 100); - }} - onError={(error) => { - authData.error = error; - authData.isConnected = false; - }} - /> - {:else if currentStep === 1} - - {:else if currentStep === 2} - - {:else if currentStep === 3} - - {/if} - - {#if errors.submit} -
-

{errors.submit}

-
- {/if} -
- - - + +
+ {#if currentStep === 0} + { + authData.error = null; + authData.token = token; + authData.isConnected = true; + setTimeout(checkGoogleAuth, 100); + }} + onError={(error) => { + authData.error = error; + authData.isConnected = false; + }} + /> + {:else if currentStep === 1} + + {:else if currentStep === 2} + + {:else if currentStep === 3} + + {/if}
+ + + diff --git a/src/routes/private/events/event/new/components/EmailSettingsStep.svelte b/src/routes/private/events/event/new/components/EmailSettingsStep.svelte index 39da6a9..9476fd8 100644 --- a/src/routes/private/events/event/new/components/EmailSettingsStep.svelte +++ b/src/routes/private/events/event/new/components/EmailSettingsStep.svelte @@ -54,7 +54,6 @@ Detected templates: {bodyTemplatesDetected.map((v) => v.name).join(', ')}

{/if} -
diff --git a/src/routes/private/events/event/new/components/GoogleAuthStep.svelte b/src/routes/private/events/event/new/components/GoogleAuthStep.svelte index ec72a5e..a88a940 100644 --- a/src/routes/private/events/event/new/components/GoogleAuthStep.svelte +++ b/src/routes/private/events/event/new/components/GoogleAuthStep.svelte @@ -21,7 +21,5 @@ onSuccess={onSuccess} onError={onError} /> - -
diff --git a/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte b/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte index ed9451a..8144140 100644 --- a/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte +++ b/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte @@ -255,8 +255,6 @@ {/if} {/if} - - {#if sheetsData.selectedSheet && sheetsData.sheetData.length > 0} @@ -370,8 +368,6 @@
Loading sheet data...
{/if} - - From 6466665549c4861148dfc619cafcd7d66ee12f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Kr=C4=8Dek?= Date: Mon, 14 Jul 2025 14:56:49 +0200 Subject: [PATCH 3/8] Redirect now directly to the event --- .../private/events/event/new/+page.svelte | 30 ++++++++++++++++--- .../new/components/GoogleSheetsStep.svelte | 9 +++--- .../event/new/components/StepNavigator.svelte | 2 +- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/routes/private/events/event/new/+page.svelte b/src/routes/private/events/event/new/+page.svelte index 41739bf..dcaf5df 100644 --- a/src/routes/private/events/event/new/+page.svelte +++ b/src/routes/private/events/event/new/+page.svelte @@ -384,13 +384,26 @@ sheetsData.expandedSheetList = !sheetsData.expandedSheetList; } + // Reset sheet selection and show sheet list + function resetSheetSelection() { + sheetsData.selectedSheet = null; + sheetsData.sheetData = []; + sheetsData.columnMapping = { + name: 0, + surname: 0, + email: 0, + confirmation: 0 + }; + sheetsData.expandedSheetList = true; + } + // Final submission async function createEvent() { if (!validateCurrentStep()) return; loading = true; try { - const { error } = await data.supabase.rpc('create_event', { + const { data: newEvent, error } = await data.supabase.rpc('create_event', { p_name: eventData.name, p_date: eventData.date, p_email_subject: emailData.subject, @@ -404,11 +417,19 @@ if (error) throw error; - // Redirect to events list or show success message - goto('/private/events'); + // Display success message + toast.success(`Event "${eventData.name}" was created successfully`); + + // Redirect to the event view page using the returned event ID + if (newEvent) { + goto(`/private/events/event/view?id=${newEvent.id}`); + } else { + // Fallback to events list if for some reason the event ID wasn't returned + goto('/private/events'); + } } catch (error) { console.error('Error creating event:', error); - errors.submit = 'Failed to create event. Please try again.'; + toast.error('Failed to create event. Please try again.'); } finally { loading = false; } @@ -453,6 +474,7 @@ {loadRecentSheets} {selectSheet} {toggleSheetList} + {resetSheetSelection} /> {:else if currentStep === 3} diff --git a/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte b/src/routes/private/events/event/new/components/GoogleSheetsStep.svelte index 8144140..97fcd65 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(), loadRecentSheets, selectSheet, toggleSheetList } = $props<{ + let { sheetsData = $bindable(), loadRecentSheets, selectSheet, toggleSheetList, resetSheetSelection } = $props<{ sheetsData: { availableSheets: GoogleSheet[]; selectedSheet: GoogleSheet | null; @@ -19,6 +19,7 @@ loadRecentSheets: () => Promise; selectSheet: (sheet: GoogleSheet) => Promise; toggleSheetList: () => void; + resetSheetSelection: () => void; }>(); // Search functionality @@ -123,13 +124,13 @@ diff --git a/src/routes/private/events/event/new/components/StepNavigator.svelte b/src/routes/private/events/event/new/components/StepNavigator.svelte index f26fb87..53d3737 100644 --- a/src/routes/private/events/event/new/components/StepNavigator.svelte +++ b/src/routes/private/events/event/new/components/StepNavigator.svelte @@ -6,7 +6,7 @@ }>(); -
+
{#each Array(totalSteps) as _, index}
From f14213a5d4687591465a7bd453028810f5e18db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Kr=C4=8Dek?= Date: Mon, 14 Jul 2025 15:50:07 +0200 Subject: [PATCH 4/8] Add role base access control for events module --- src/app.d.ts | 13 +++++++++ src/hooks.server.ts | 28 +++++++++++++++++++ src/routes/+layout.server.ts | 13 +++++++-- src/routes/+layout.ts | 7 ++++- src/routes/+page.svelte | 1 - src/routes/private/+layout.svelte | 6 +++- .../private/errors/events/denied/+page.svelte | 1 + src/routes/private/home/+page.server.ts | 22 --------------- src/routes/private/home/+page.svelte | 13 +++------ 9 files changed, 68 insertions(+), 36 deletions(-) create mode 100644 src/routes/private/errors/events/denied/+page.svelte delete mode 100644 src/routes/private/home/+page.server.ts diff --git a/src/app.d.ts b/src/app.d.ts index 9a0cf33..4aa1e3d 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,17 +1,30 @@ import type { Session, SupabaseClient, User } from '@supabase/supabase-js' import type { Database } from './database.types.ts' // import generated types +// Define the profile type based on the database schema +type Profile = { + display_name: string | null + section_position: string | null + section: { + name: string | null + } | null +} + declare global { namespace App { // interface Error {} interface Locals { supabase: SupabaseClient safeGetSession: () => Promise<{ session: Session | null; user: User | null }> + getUserProfile: (userId: string) => Promise session: Session | null user: User | null + profile: Profile | null } interface PageData { session: Session | null + user: User | null + profile: Profile | null } // interface PageState {} // interface Platform {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 09e2560..ba7a5a8 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -51,6 +51,22 @@ const supabase: Handle = async ({ event, resolve }) => { return { session, user } } + /** + * Fetch user profile data including display name, section position, and section name + */ + event.locals.getUserProfile = async (userId) => { + if (!userId) return null + + const { data: profile, error } = await event.locals.supabase + .from('profiles') + .select('display_name, section_position, section:sections (name)') + .eq('id', userId) + .single() + + if (error) return null + return profile + } + return resolve(event, { filterSerializedResponseHeaders(name) { /** @@ -67,6 +83,11 @@ const authGuard: Handle = async ({ event, resolve }) => { event.locals.session = session event.locals.user = user + // Fetch the user's profile if they're authenticated + if (user) { + event.locals.profile = await event.locals.getUserProfile(user.id) + } + if (!event.locals.session && event.url.pathname.startsWith('/private')) { redirect(303, '/auth') } @@ -75,6 +96,13 @@ const authGuard: Handle = async ({ event, resolve }) => { redirect(303, '/private/home') } + // Role-based access control for events routes + if (event.url.pathname.startsWith('/private/events')) { + if (!event.locals.profile || event.locals.profile.section_position !== 'events_manager') { + redirect(303, '/private/errors/events/denied') + } + } + return resolve(event) } diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 7543dfa..8277089 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,9 +1,18 @@ import type { LayoutServerLoad } from './$types' -export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => { - const { session } = await safeGetSession() +export const load: LayoutServerLoad = async ({ locals: { safeGetSession, getUserProfile }, cookies }) => { + const { session, user } = await safeGetSession() + + // Get the user profile if the user is authenticated + let profile = null + if (user) { + profile = await getUserProfile(user.id) + } + return { session, + user, + profile, cookies: cookies.getAll(), } } \ No newline at end of file diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 46d5417..e19c00c 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -39,5 +39,10 @@ export const load: LayoutLoad = async ({ data, depends, fetch }) => { data: { user }, } = await supabase.auth.getUser() - return { session, supabase, user } + return { + session, + supabase, + user, + profile: data.profile + } } \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a09321c..f22a586 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,5 +1,4 @@
-
diff --git a/src/routes/private/+layout.svelte b/src/routes/private/+layout.svelte index f45a903..748cf77 100644 --- a/src/routes/private/+layout.svelte +++ b/src/routes/private/+layout.svelte @@ -3,6 +3,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'; import ToastContainer from '$lib/components/ToastContainer.svelte'; + let { data } = $props(); + const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -22,7 +24,9 @@
diff --git a/src/routes/private/errors/events/denied/+page.svelte b/src/routes/private/errors/events/denied/+page.svelte new file mode 100644 index 0000000..884187a --- /dev/null +++ b/src/routes/private/errors/events/denied/+page.svelte @@ -0,0 +1 @@ +Access to events denied! \ No newline at end of file diff --git a/src/routes/private/home/+page.server.ts b/src/routes/private/home/+page.server.ts deleted file mode 100644 index 56a2a40..0000000 --- a/src/routes/private/home/+page.server.ts +++ /dev/null @@ -1,22 +0,0 @@ -// src/routes/my-page/+page.server.ts -import type { PageServerLoad } from './$types'; - -export const load: PageServerLoad = async ({ locals }) => { - // get the logged-in user - const { data: { user }, error: authError } = await locals.supabase.auth.getUser(); - - const { data: user_profile, error: profileError } = await locals.supabase.from('profiles').select('*, section:sections (id, name)').eq('id', user?.id).single(); - - if (authError) { - console.error('Supabase auth error:', authError); - throw new Error('Could not get user'); - } - - if (profileError) { - console.error('Supabase profile error:', profileError); - throw new Error('Could not get user profile'); - } - - return { user, user_profile }; - -}; \ No newline at end of file diff --git a/src/routes/private/home/+page.svelte b/src/routes/private/home/+page.svelte index d081370..9ac6c24 100644 --- a/src/routes/private/home/+page.svelte +++ b/src/routes/private/home/+page.svelte @@ -1,10 +1,5 @@

User Profile

@@ -16,18 +11,18 @@ {data.user?.user_metadata.display_name?.[0] ?? "U"}
- {data.user?.user_metadata.display_name} + {data.profile?.display_name}
{data.user?.email}
Section: - {data.user_profile?.section.name ?? "N/A"} + {data.profile?.section.name ?? "N/A"}
Position: - {data.user_profile?.section_position ?? "N/A"} + {data.profile?.section_position ?? "N/A"}

User guide

From d0f555a7c5ecfceccf15bcc15c857934eb88c969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Kr=C4=8Dek?= Date: Mon, 14 Jul 2025 16:05:29 +0200 Subject: [PATCH 5/8] Minor styling changes --- src/routes/private/+layout.svelte | 7 +- .../private/errors/events/denied/+page.svelte | 51 +++++++++++- src/routes/private/home/+page.svelte | 78 ++++++++++--------- 3 files changed, 96 insertions(+), 40 deletions(-) diff --git a/src/routes/private/+layout.svelte b/src/routes/private/+layout.svelte index 748cf77..4cba364 100644 --- a/src/routes/private/+layout.svelte +++ b/src/routes/private/+layout.svelte @@ -3,7 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'; import ToastContainer from '$lib/components/ToastContainer.svelte'; - let { data } = $props(); + let { data, children } = $props(); const queryClient = new QueryClient({ defaultOptions: { @@ -19,10 +19,9 @@