diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 084af10..be05df8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,9 +1,11 @@ GitHub Copilot Instructions for This Repository -Use Svelte 5 runes exclusively -Declare reactive state with $state(); derive values with $derived(); run side-effect logic with $effect() etc. -svelte.dev -svelte.dev +Basics: +- If you have any questions, always ask me first! +- Use Svelte 5 runes exclusively +- Declare reactive state with $state(); derive values with $derived(); run side-effect logic with $effect() etc. +- When doing client-side loading, always implement placeholders and loaders, so the UI remains responsive and layout shifts are minimized. + - Don't use placeholders and loaders for static data like heading etc. Do not fall back to the legacy $: label syntax or Svelte 3/4 stores! This is important! diff --git a/src/routes/api/auth/refresh/+server.ts b/src/routes/api/auth/refresh/+server.ts deleted file mode 100644 index cfbd733..0000000 --- a/src/routes/api/auth/refresh/+server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { getOAuthClient } from '$lib/google/auth/server.js'; - -export const POST: RequestHandler = async ({ request }) => { - try { - const { refreshToken } = await request.json(); - - if (!refreshToken) { - return json({ error: 'Refresh token is required' }, { status: 400 }); - } - - const oauth = getOAuthClient(); - oauth.setCredentials({ refresh_token: refreshToken }); - - const { credentials } = await oauth.refreshAccessToken(); - - if (!credentials.access_token) { - return json({ error: 'Failed to refresh token' }, { status: 500 }); - } - - return json({ - accessToken: credentials.access_token, - expiresIn: credentials.expiry_date - }); - } catch (error) { - console.error('Error refreshing access token:', error); - return json({ error: 'Failed to refresh access token' }, { status: 500 }); - } -}; diff --git a/src/routes/api/events/+server.ts b/src/routes/api/events/+server.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/api/sheets/[sheetId]/data/+server.ts b/src/routes/api/sheets/[sheetId]/data/+server.ts deleted file mode 100644 index 9e9b4b0..0000000 --- a/src/routes/api/sheets/[sheetId]/data/+server.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { googleSheetsServer } from '$lib/google/sheets/server'; - -export const GET: RequestHandler = async ({ params, request }) => { - try { - const { sheetId } = params; - const authHeader = request.headers.get('authorization'); - - if (!authHeader?.startsWith('Bearer ')) { - return json({ error: 'Missing or invalid authorization header' }, { status: 401 }); - } - - const refreshToken = authHeader.slice(7); - const sheetData = await googleSheetsServer.getSpreadsheetData(refreshToken, sheetId, 'A1:Z10'); - - return json(sheetData); - } catch (error) { - console.error('Error fetching spreadsheet data:', error); - return json({ error: 'Failed to fetch spreadsheet data' }, { status: 500 }); - } -}; diff --git a/src/routes/api/sheets/recent/+server.ts b/src/routes/api/sheets/recent/+server.ts deleted file mode 100644 index d3eba61..0000000 --- a/src/routes/api/sheets/recent/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { googleSheetsServer } from '$lib/google/sheets/server'; - -export const GET: RequestHandler = async ({ request }) => { - try { - const authHeader = request.headers.get('authorization'); - if (!authHeader?.startsWith('Bearer ')) { - return json({ error: 'Missing or invalid authorization header' }, { status: 401 }); - } - - const refreshToken = authHeader.slice(7); - const spreadsheets = await googleSheetsServer.getRecentSpreadsheets(refreshToken, 20); - - return json(spreadsheets); - } catch (error) { - console.error('Error fetching recent spreadsheets:', error); - return json({ error: 'Failed to fetch spreadsheets' }, { status: 500 }); - } -}; diff --git a/src/routes/private/api/gmail/+server.ts b/src/routes/private/api/google/gmail/+server.ts similarity index 100% rename from src/routes/private/api/gmail/+server.ts rename to src/routes/private/api/google/gmail/+server.ts diff --git a/src/routes/private/events/event/archived/+page.svelte b/src/routes/private/events/event/archived/+page.svelte index 37cdb56..050d66d 100644 --- a/src/routes/private/events/event/archived/+page.svelte +++ b/src/routes/private/events/event/archived/+page.svelte @@ -1,6 +1,8 @@ + +
+
+ {#if loading} +
+
+ {:else} +

{event?.name}

+
+
+ Date: + {event?.date} +
+
+ {/if} +
+
diff --git a/src/routes/private/events/event/archived/components/Statistics.svelte b/src/routes/private/events/event/archived/components/Statistics.svelte new file mode 100644 index 0000000..526483b --- /dev/null +++ b/src/routes/private/events/event/archived/components/Statistics.svelte @@ -0,0 +1,73 @@ + + +
+ + + + + + + + + + + + + + + + + + + + +
CategoryCount
+
+ + + + Total participants +
+
+ {#if loading} +
+ {:else} + {totalParticipants} + {/if} +
+
+ + + + Scanned participants +
+
+ {#if loading} +
+ {:else} + {scannedParticipants} + {/if} +
+
diff --git a/src/routes/private/events/event/view/+page.svelte b/src/routes/private/events/event/view/+page.svelte index 51fd7f2..5a36662 100644 --- a/src/routes/private/events/event/view/+page.svelte +++ b/src/routes/private/events/event/view/+page.svelte @@ -1,7 +1,16 @@ @@ -249,360 +260,50 @@

Event Overview

- -
- {#if loading} - -
-
-
-
-
-
- {:else if event} -
-
-

{event.name}

-
-
- Date: - {formatDate(event.date)} -
-
- Created: - {formatDate(event.created_at)} -
- -
-
-
-

Email Details

-
-
- Subject: -

{event.email_subject}

-
-
- Body: -

{event.email_body}

-
-
-
-
- {:else if error} -
-

{error}

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

Google Account

-

Required for syncing participants and sending emails

-
- { - // Refresh the page or update UI state as needed - error = ''; - }} - onError={(errorMsg) => { - error = errorMsg; - }} + + + + +
+

Statistics

+ p.scanned).length} + emailSentCount={participants.filter(p => p.email_sent).length} + pendingCount={participants.filter(p => !p.email_sent).length} />
- -
-
-

Participants

- -
+ - {#if participantsLoading} - -
- {#each Array(5) as _} -
-
-
-
-
-
-
- {/each} -
- {:else if participants.length > 0} -
- - - - - - - - - - - - {#each participants as participant} - - - - - - - - {/each} - -
NameSurnameEmailScannedEmail Sent
{participant.name}{participant.surname}{participant.email} - {#if participant.scanned} -
- - - -
- {:else} -
- - - -
- {/if} -
- {#if participant.email_sent} -
- - - -
- {:else} -
- - - -
- {/if} -
-
- {:else} -
-

- No participants found. Click "Sync Participants" to load from Google Sheets. -

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

Send Emails

-
- {participants.filter(p => !p.email_sent).length} uncontacted participants -
-
- - {#if sendingEmails} -
-
- - - - - Sending {emailProgress.total} emails... Please wait. -
-
- {:else} -
- {#if participants.filter(p => !p.email_sent).length > 0} -
-
- - - - - Warning: Do not close this window while emails are being sent. The process may take several minutes. - -
-
- -
- -
- {:else} -
-
- - - -
-

All participants have been contacted!

-

No pending emails to send.

-
- {/if} -
- {/if} -
- - {#if emailResults} -
-
-

Email Results

-
- {emailResults.summary.success} successful, {emailResults.summary.errors} failed -
-
- -
-
-
-
- Sent: {emailResults.summary.success} -
-
-
- Failed: {emailResults.summary.errors} -
-
-
- Total: {emailResults.summary.total} -
-
-
- - {#if emailResults.results.length > 0} -
- - - - - - - - - - {#each emailResults.results as result} - - - - - - {/each} - -
NameEmailStatus
- {result.participant.name} {result.participant.surname} - {result.participant.email} - {#if result.success} -
- - - - Sent -
- {:else} -
- - - - Failed - {#if result.error} - ({result.error}) - {/if} -
- {/if} -
-
- {/if} -
+ {/if} {#if error} -
-

{error}

-
+ {/if} diff --git a/src/routes/private/events/event/view/components/EmailResults.svelte b/src/routes/private/events/event/view/components/EmailResults.svelte new file mode 100644 index 0000000..1e2c2c7 --- /dev/null +++ b/src/routes/private/events/event/view/components/EmailResults.svelte @@ -0,0 +1,100 @@ + + +{#if emailResults} +
+
+

Email Results

+
+ {emailResults.summary.success} successful, {emailResults.summary.errors} failed +
+
+ +
+
+
+
+ Sent: {emailResults.summary.success} +
+
+
+ Failed: {emailResults.summary.errors} +
+
+
+ Total: {emailResults.summary.total} +
+
+
+ + {#if emailResults.results.length > 0} +
+ + + + + + + + + + {#each emailResults.results as result} + + + + + + {/each} + +
NameEmailStatus
+ {result.participant.name} {result.participant.surname} + {result.participant.email} + {#if result.success} +
+ + + + Sent +
+ {:else} +
+ + + + Failed + {#if result.error} + ({result.error}) + {/if} +
+ {/if} +
+
+ {/if} +
+{/if} diff --git a/src/routes/private/events/event/view/components/EmailSending.svelte b/src/routes/private/events/event/view/components/EmailSending.svelte new file mode 100644 index 0000000..0e27113 --- /dev/null +++ b/src/routes/private/events/event/view/components/EmailSending.svelte @@ -0,0 +1,94 @@ + + +
+
+

Send Emails

+ {#if !loading} +
+ {uncontactedParticipantsCount} uncontacted participants +
+ {:else} +
+ Loading participants... +
+ {/if} +
+ + {#if loading} +
+ {:else if sendingEmails} +
+
+ + + + + Sending {emailProgress.total} emails... Please wait. +
+
+ {:else} +
+ {#if uncontactedParticipantsCount > 0} +
+
+ + + + + Warning: Do not close this window while emails are being sent. The process may take several minutes. + +
+
+ +
+ +
+ {:else} +
+
+ + + +
+

All participants have been contacted!

+

No pending emails to send.

+
+ {/if} +
+ {/if} +
diff --git a/src/routes/private/events/event/view/components/EmailSendingFixed.svelte b/src/routes/private/events/event/view/components/EmailSendingFixed.svelte new file mode 100644 index 0000000..0e27113 --- /dev/null +++ b/src/routes/private/events/event/view/components/EmailSendingFixed.svelte @@ -0,0 +1,94 @@ + + +
+
+

Send Emails

+ {#if !loading} +
+ {uncontactedParticipantsCount} uncontacted participants +
+ {:else} +
+ Loading participants... +
+ {/if} +
+ + {#if loading} +
+ {:else if sendingEmails} +
+
+ + + + + Sending {emailProgress.total} emails... Please wait. +
+
+ {:else} +
+ {#if uncontactedParticipantsCount > 0} +
+
+ + + + + Warning: Do not close this window while emails are being sent. The process may take several minutes. + +
+
+ +
+ +
+ {:else} +
+
+ + + +
+

All participants have been contacted!

+

No pending emails to send.

+
+ {/if} +
+ {/if} +
diff --git a/src/routes/private/events/event/view/components/EmailTemplate.svelte b/src/routes/private/events/event/view/components/EmailTemplate.svelte new file mode 100644 index 0000000..2e9718e --- /dev/null +++ b/src/routes/private/events/event/view/components/EmailTemplate.svelte @@ -0,0 +1,47 @@ + + +
+
+

Email Template

+
+ + {#if loading} + +
+
+ Subject: +
+
+ +
+ Body: +
+
+
+ {:else if event} +
+
+ Subject: +
+

{event.email_subject}

+
+
+
+ Body: +
+

{event.email_body}

+
+
+
+ {/if} +
diff --git a/src/routes/private/events/event/view/components/ErrorMessage.svelte b/src/routes/private/events/event/view/components/ErrorMessage.svelte new file mode 100644 index 0000000..e222576 --- /dev/null +++ b/src/routes/private/events/event/view/components/ErrorMessage.svelte @@ -0,0 +1,11 @@ + + +{#if error} +
+

{error}

+
+{/if} diff --git a/src/routes/private/events/event/view/components/EventInformation.svelte b/src/routes/private/events/event/view/components/EventInformation.svelte new file mode 100644 index 0000000..7d99aac --- /dev/null +++ b/src/routes/private/events/event/view/components/EventInformation.svelte @@ -0,0 +1,88 @@ + + +
+ {#if loading} + +
+

Event Information

+
+
+ Date: +
+
+
+ Created: +
+
+
+ Sheet ID: +
+
+
+
+ {:else if event} +
+

{event.name}

+
+
+ Date: + {formatDate(event.date)} +
+
+ Created: + {formatDate(event.created_at)} +
+ +
+
+ {:else if error} +
+

{error}

+
+ {/if} +
diff --git a/src/routes/private/events/event/view/components/GoogleAuthentication.svelte b/src/routes/private/events/event/view/components/GoogleAuthentication.svelte new file mode 100644 index 0000000..4c51af1 --- /dev/null +++ b/src/routes/private/events/event/view/components/GoogleAuthentication.svelte @@ -0,0 +1,26 @@ + + +
+
+

Google Account

+

Required for syncing participants and sending emails

+
+ {#if loading} +
+ {:else} + + {/if} +
diff --git a/src/routes/private/events/event/view/components/ParticipantsTable.svelte b/src/routes/private/events/event/view/components/ParticipantsTable.svelte new file mode 100644 index 0000000..1660920 --- /dev/null +++ b/src/routes/private/events/event/view/components/ParticipantsTable.svelte @@ -0,0 +1,162 @@ + + +
+
+

Participants

+ +
+ + {#if participantsLoading || loading} + +
+ {#each Array(5) as _} +
+
+
+
+
+
+
+ {/each} +
+ {:else if participants.length > 0} +
+ + + + + + + + + + + + {#each participants as participant} + + + + + + + + {/each} + +
NameSurnameEmailScannedEmail Sent
{participant.name}{participant.surname}{participant.email} + {#if participant.scanned} +
+ + + +
+ {:else} +
+ + + +
+ {/if} +
+ {#if participant.email_sent} +
+ + + +
+ {:else} +
+ + + +
+ {/if} +
+
+ {:else} +
+

+ No participants found. Click "Sync Participants" to load from Google Sheets. +

+
+ {/if} +
diff --git a/src/routes/private/events/event/view/components/Statistics.svelte b/src/routes/private/events/event/view/components/Statistics.svelte new file mode 100644 index 0000000..f657d59 --- /dev/null +++ b/src/routes/private/events/event/view/components/Statistics.svelte @@ -0,0 +1,127 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryCount
+
+ + + + Total participants +
+
+ {#if loading} +
+ {:else} + {totalCount} + {/if} +
+
+ + + + Scanned participants +
+
+ {#if loading} +
+ {:else} + {scannedCount} + {/if} +
+
+ + + + Email sent +
+
+ {#if loading} +
+ {:else} + {emailSentCount} + {/if} +
+
+ + + + Pending emails +
+
+ {#if loading} +
+ {:else} + {pendingCount} + {/if} +
+
diff --git a/src/routes/private/scanner/+page.svelte b/src/routes/private/scanner/+page.svelte index 8220dab..36fad5f 100644 --- a/src/routes/private/scanner/+page.svelte +++ b/src/routes/private/scanner/+page.svelte @@ -7,21 +7,21 @@ import { ScanState, defaultTicketData } from '$lib/types/types'; let { data } = $props(); - let scanned_id = $state(""); + let scanned_id = $state(''); let ticket_data = $state(defaultTicketData); let scan_state = $state(ScanState.scanning); - + // Events related state interface Event { id: string; name: string; date: string; } - + let events = $state([]); - let selectedEventId = $state(""); + let selectedEventId = $state(''); let isLoadingEvents = $state(true); - let eventsError = $state(""); + let eventsError = $state(''); onMount(async () => { await loadEvents(); @@ -30,7 +30,7 @@ async function loadEvents() { isLoadingEvents = true; eventsError = ''; - + try { const { data: eventsData, error } = await data.supabase .from('events') @@ -39,7 +39,7 @@ if (error) throw error; events = eventsData || []; - + // If there are events, select the first one by default if (events.length > 0) { selectedEventId = events[0].id; @@ -54,20 +54,20 @@ // Process a scanned QR code $effect(() => { - if (scanned_id === "") return; + if (scanned_id === '') return; scan_state = ScanState.scanning; - console.log("Scanned ID:", scanned_id); + console.log('Scanned ID:', scanned_id); data.supabase .from('participants') .select(`*, event ( id, name ), scanned_by ( id, display_name )`) .eq('id', scanned_id) - .then(response => { + .then((response) => { if (response.data && response.data.length > 0) { const participant = response.data[0]; ticket_data = participant; - + // Check if the participant belongs to the selected event if (selectedEventId && participant.event.id !== selectedEventId) { scan_state = ScanState.wrong_event; @@ -81,73 +81,52 @@ ticket_data = defaultTicketData; scan_state = ScanState.scan_failed; } - + // Reset the scanned_id after 3 seconds to allow for a new scan setTimeout(() => { - scanned_id = ""; + scanned_id = ''; }, 3000); }); }); -
-

Code Scanner

+

Code Scanner

- -
-

Select Event

- {#if isLoadingEvents} -
-
-
- {:else if eventsError} -
- {eventsError} - -
- {:else if events.length === 0} -

No events found

- {:else} - - {/if} -
- - -
- -
- - -

Ticket Information

- - - - {#if scan_state !== ScanState.scanning} -
- + +
+

Select Event

+ {#if isLoadingEvents} +
+
+ {:else if eventsError} +
+ {eventsError} + +
+ {:else if events.length === 0} +

No events found

+ {:else} + {/if}
+ + +
+ +
+ + +

Ticket Information

+ diff --git a/src/routes/private/scanner/TicketDisplay.svelte b/src/routes/private/scanner/TicketDisplay.svelte index 053b66b..9f0446a 100644 --- a/src/routes/private/scanner/TicketDisplay.svelte +++ b/src/routes/private/scanner/TicketDisplay.svelte @@ -15,65 +15,73 @@ } -
- {#if scan_state === ScanState.scanning} -
-
-

Waiting for QR code...

-
- {:else if scan_state === ScanState.scan_failed} -
-
- - - -

Invalid Code

+
+
+ {#if scan_state === ScanState.scanning} +
+
+

Waiting for QR code...

-

This QR code is not a valid ticket or doesn't exist in our system.

-
- {:else if scan_state === ScanState.wrong_event} -
-
- - - -

Wrong Event

+ {:else if scan_state === ScanState.scan_failed} +
+
+ + + +

Invalid Code

+
+

This QR code is not a valid ticket or doesn't exist in our system.

+
+ + + +
-

This ticket belongs to a different event:

-
-

{ticket_data.event.name}

-

{ticket_data.name} {ticket_data.surname}

+ {:else if scan_state === ScanState.wrong_event} +
+
+ + + +

Wrong Event

+
+

This ticket belongs to a different event:

+
+

{ticket_data.event?.name || ''}

+

{ticket_data.name || ''} {ticket_data.surname || ''}

+
-
- {:else if scan_state === ScanState.already_scanned} -
-
- - - -

Already Scanned

+ {:else if scan_state === ScanState.already_scanned} +
+
+ + + +

Already Scanned

+
+

+ This ticket was already scanned by:
{ticket_data.scanned_by?.display_name || 'someone'} + {ticket_data.scanned_at ? `on ${formatScannedAt(ticket_data.scanned_at)}` : ''} +

+
+

{ticket_data.event?.name || ''}

+

{ticket_data.name || ''} {ticket_data.surname || ''}

+
-

- This ticket was already scanned by {ticket_data.scanned_by?.display_name || 'someone'} - {ticket_data.scanned_at ? `on ${formatScannedAt(ticket_data.scanned_at)}` : ''} -

-
-

{ticket_data.event.name}

-

{ticket_data.name} {ticket_data.surname}

+ {:else if scan_state === ScanState.scan_successful} +
+
+ + + +

Valid Ticket

+
+

Ticket successfully validated.

+
+

{ticket_data.event?.name || ''}

+

{ticket_data.name || ''} {ticket_data.surname || ''}

+
-
- {:else if scan_state === ScanState.scan_successful} -
-
- - - -

Valid Ticket

-
-
-

{ticket_data.event.name}

-

{ticket_data.name} {ticket_data.surname}

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