diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 229b045..0302bc4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,6 +9,12 @@ 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. +- to add a notification, use the toast component + - example: toast.success, toast.info, toast.warning, toast.error + + Do not fall back to the legacy $: label syntax or Svelte 3/4 stores! This is important! 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/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/+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 2f63c7b..4cba364 100644 --- a/src/routes/private/+layout.svelte +++ b/src/routes/private/+layout.svelte @@ -1,6 +1,9 @@ + +
+
+
+
+ + + +
+

Access Denied

+

You don't have permission to access the events section.

+
+ +
+ {#if profile} +

+ Your current role: {profile.section_position || 'Not assigned'} +

+ {#if profile.section} +

+ Section: {profile.section.name} +

+ {/if} + {/if} + +

+ You need the events_manager role to access this section. + Please contact your administrator for assistance. +

+
+ +
+ + Go to Dashboard + + +
+
+
\ No newline at end of file diff --git a/src/routes/private/events/event/new/+page.svelte b/src/routes/private/events/event/new/+page.svelte index 3d9f809..dcaf5df 100644 --- a/src/routes/private/events/event/new/+page.svelte +++ b/src/routes/private/events/event/new/+page.svelte @@ -3,7 +3,8 @@ import { isTokenValid, getUserInfo, revokeToken } from '$lib/google/auth/client.js'; 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'; @@ -41,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 @@ -63,7 +64,7 @@ onMount(async () => { // Check Google auth status on mount await checkGoogleAuth(); - + if (currentStep === 2) { await loadRecentSheets(); } @@ -75,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); @@ -103,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) { @@ -126,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(); @@ -140,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); @@ -189,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 @@ -216,7 +217,7 @@ authData.userEmail = null; } } - + async function disconnectGoogle() { try { // First revoke the token at Google using our API @@ -224,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; @@ -259,49 +260,65 @@ } function validateCurrentStep(): boolean { + // Clear previous errors errors = {}; - + let isValid = true; + if (currentStep === 0) { if (!authData.isConnected) { + toast.error('Please connect your Google account to continue'); errors.auth = 'Please connect your Google account to continue'; return false; } } else if (currentStep === 1) { if (!eventData.name.trim()) { + toast.error('Event name is required'); errors.name = 'Event name is required'; + isValid = false; } if (!eventData.date) { + toast.error('Event date is required'); errors.date = 'Event date is required'; + isValid = false; } } else if (currentStep === 2) { if (!sheetsData.selectedSheet) { + toast.error('Please select a Google Sheet'); 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) { - errors.sheetData = `Please map the following columns: ${missingColumns.join(', ')}`; + const errorMsg = `Please map the following columns: ${missingColumns.join(', ')}`; + toast.error(errorMsg); + errors.sheetData = errorMsg; + isValid = false; } } } else if (currentStep === 3) { if (!emailData.subject.trim()) { + toast.error('Email subject is required'); errors.subject = 'Email subject is required'; + isValid = false; } if (!emailData.body.trim()) { + toast.error('Email body is required'); errors.body = 'Email body is required'; + isValid = false; } } - return Object.keys(errors).length === 0; + return isValid; } // Google Sheets functions @@ -309,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(); } @@ -332,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 || []; @@ -361,19 +378,32 @@ sheetsData.loading = false; } } - + // Toggle the sheet list expansion function toggleSheetList() { 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, @@ -387,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; } @@ -410,49 +448,46 @@ }); -
- - + + - -
- {#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 e946b81..9476fd8 100644 --- a/src/routes/private/events/event/new/components/EmailSettingsStep.svelte +++ b/src/routes/private/events/event/new/components/EmailSettingsStep.svelte @@ -1,10 +1,9 @@ @@ -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..a88a940 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; }>(); @@ -22,11 +21,5 @@ onSuccess={onSuccess} 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..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(), errors = $bindable(), loadRecentSheets, selectSheet, toggleSheetList } = $props<{ + let { sheetsData = $bindable(), loadRecentSheets, selectSheet, toggleSheetList, resetSheetSelection } = $props<{ sheetsData: { availableSheets: GoogleSheet[]; selectedSheet: GoogleSheet | null; @@ -16,10 +16,10 @@ loading: boolean; expandedSheetList: boolean; }; - errors: Record; loadRecentSheets: () => Promise; selectSheet: (sheet: GoogleSheet) => Promise; toggleSheetList: () => void; + resetSheetSelection: () => void; }>(); // Search functionality @@ -124,13 +124,13 @@ @@ -256,10 +256,6 @@ {/if} {/if} - - {#if errors.sheet} -

{errors.sheet}

- {/if} {#if sheetsData.selectedSheet && sheetsData.sheetData.length > 0} @@ -373,10 +369,6 @@
Loading sheet data...
{/if} - - {#if errors.sheetData} -

{errors.sheetData}

- {/if} 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}
diff --git a/src/routes/private/events/event/view/+page.svelte b/src/routes/private/events/event/view/+page.svelte index 5a36662..482450c 100644 --- a/src/routes/private/events/event/view/+page.svelte +++ b/src/routes/private/events/event/view/+page.svelte @@ -9,8 +9,8 @@ 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 { toast } from '$lib/stores/toast.js'; let { data } = $props(); @@ -47,9 +47,9 @@ let participantsLoading = $state(true); let syncingParticipants = $state(false); let sendingEmails = $state(false); + let updatingEmail = $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,7 @@ event = eventData; } catch (err) { console.error('Error loading event:', err); - error = 'Failed to load event'; + toast.error('Failed to load event'); } finally { loading = false; } @@ -95,24 +95,26 @@ participants = participantsData || []; } catch (err) { console.error('Error loading participants:', err); - error = 'Failed to load participants'; + toast.error('Failed to load participants'); } finally { participantsLoading = false; } } async function syncParticipants() { - if (!event || !event.sheet_id) return; + if (!event || !event.sheet_id) { + toast.error('Cannot sync participants: No Google Sheet is connected to this event'); + return; + } // 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.error('Please connect your Google account first to sync participants'); return; } syncingParticipants = true; - error = ''; try { // Fetch sheet data const response = await fetch(`/private/api/google/sheets/${event.sheet_id}/data`, { @@ -175,9 +177,19 @@ // Reload participants await loadParticipants(); + + // Show success message with count of synced participants + const previousCount = participants.length; + const newCount = names.length; + const addedCount = Math.max(0, participants.length - previousCount); + + toast.success( + `Successfully synced participants. ${newCount} entries processed, ${addedCount} new participants added.`, + 5000 + ); } catch (err) { console.error('Error syncing participants:', err); - error = 'Failed to sync participants'; + toast.error(`Failed to sync participants: ${err instanceof Error ? err.message : 'Unknown error'}`); } finally { syncingParticipants = false; } @@ -189,20 +201,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 +252,76 @@ }); } 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; } } + // For Email Template updating + async function handleEmailUpdate(eventId: string, subject: string, body: string) { + updatingEmail = true; + + try { + // Call the email_modify RPC function + const { error } = await data.supabase.rpc('email_modify', { + p_event_id: eventId, + p_email_subject: subject, + p_email_body: body + }); + + if (error) throw error; + + // Update the local event data on success + if (event) { + event.email_subject = subject; + event.email_body = body; + } + + toast.add({ + message: 'Email template updated successfully', + type: 'success' + }); + + } catch (err) { + console.error('Error updating email template:', err); + toast.add({ + message: 'Failed to update email template', + type: 'error' + }); + } finally { + updatingEmail = 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/EmailTemplate.svelte b/src/routes/private/events/event/view/components/EmailTemplate.svelte index 2e9718e..e29f2ae 100644 --- a/src/routes/private/events/event/view/components/EmailTemplate.svelte +++ b/src/routes/private/events/event/view/components/EmailTemplate.svelte @@ -1,18 +1,104 @@
-
+

Email Template

+ {#if !loading && event} +
+ {#if isEditing} + + + {:else} + + {/if} +
+ {/if}
{#if loading} @@ -31,17 +117,34 @@ {:else if event}
- Subject: -
-

{event.email_subject}

-
+ +
+
- Body: -
-

{event.email_body}

-
+ + + {#if isEditing} +
+ Template variables: {name}, + {surname} +
+ {/if}
+ +
{/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/ParticipantsTable.svelte b/src/routes/private/events/event/view/components/ParticipantsTable.svelte index 1660920..7acb662 100644 --- a/src/routes/private/events/event/view/components/ParticipantsTable.svelte +++ b/src/routes/private/events/event/view/components/ParticipantsTable.svelte @@ -1,4 +1,6 @@
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} 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..c6a5feb 100644 --- a/src/routes/private/home/+page.svelte +++ b/src/routes/private/home/+page.svelte @@ -1,51 +1,76 @@ -

User Profile

+
+

User Dashboard

-
-
-
-
- {data.user?.user_metadata.display_name?.[0] ?? "U"} -
-
- {data.user?.user_metadata.display_name} -
{data.user?.email}
-
-
-
-
- Section: - {data.user_profile?.section.name ?? "N/A"} -
-
- Position: - {data.user_profile?.section_position ?? "N/A"} -
-
-

User guide

-

- To scan a QR code, head over to Scanner in the top right corner. Click on Start scanning and allow camera permissions. - If you close and open your browser and your camera is stuck, simply refresh the page or click Stop scanning and then Start scanning again. - When you scan a QR code, a request is sent to the server to get the user's personal information and to mark their tickets as scanned. -

-

Administrator guide

-

- You can view events -

-
+
+ +
+
+
+
+ {data.profile?.display_name?.[0]?.toUpperCase() ?? 'U'} +
+

{data.profile?.display_name}

+

{data.user?.email}

+
+
+ Sign Out +
+
+
+ + +
+ +
+

Your Role

+
+
+
Section
+
+ {data.profile?.section?.name ?? 'N/A'} +
+
+
+
Position
+
+ {data.profile?.section_position ?? 'N/A'} +
+
+
+
+ + +
+

User Guide

+

+ To scan a QR code, head over to Scanner in the top right corner. Click + on "Start Scanning" and allow camera permissions. If your camera gets stuck, simply + refresh the page or click "Stop Scanning" and then "Start Scanning" again. When you scan + a QR code, the participant's ticket is automatically marked as scanned. +

+
+ + + {#if data.profile?.section_position === 'events_manager'} +
+

Events Manager Guide

+

+ As an Events Manager, you have access to the Events section. Here you + can create new events, manage participants by syncing with Google Sheets, send email + invitations with QR codes, and view event statistics. +

+
+ {/if} +
+
- - - Sign out -