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 @@
+
+
+
+
+
+
+ | Category |
+ Count |
+
+
+
+
+
+ |
+
+
+ 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}
-
- {/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}
-
-
-
-
- | Name |
- Surname |
- Email |
- Scanned |
- Email Sent |
-
-
-
- {#each participants as participant}
-
- | {participant.name} |
- {participant.surname} |
- {participant.email} |
-
- {#if participant.scanned}
-
- {:else}
-
- {/if}
- |
-
- {#if participant.email_sent}
-
- {:else}
-
- {/if}
- |
-
- {/each}
-
-
-
- {: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}
-
-
-
-
- | Name |
- Email |
- Status |
-
-
-
- {#each emailResults.results as result}
-
- |
- {result.participant.name} {result.participant.surname}
- |
- {result.participant.email} |
-
- {#if result.success}
-
- {:else}
-
-
- Failed
- {#if result.error}
- ({result.error})
- {/if}
-
- {/if}
- |
-
- {/each}
-
-
-
- {/if}
-
+
{/if}
{#if 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}
+
+
+
+
+ | Name |
+ Email |
+ Status |
+
+
+
+ {#each emailResults.results as result}
+
+ |
+ {result.participant.name} {result.participant.surname}
+ |
+ {result.participant.email} |
+
+ {#if result.success}
+
+ {:else}
+
+
+ Failed
+ {#if result.error}
+ ({result.error})
+ {/if}
+
+ {/if}
+ |
+
+ {/each}
+
+
+
+ {/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}
+
+
+ {:else if event}
+
+
+
Subject:
+
+
{event.email_subject}
+
+
+
+
+ {/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}
+
+{/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}
+
+
+ {:else if event}
+
+
{event.name}
+
+
+ Date:
+ {formatDate(event.date)}
+
+
+ Created:
+ {formatDate(event.created_at)}
+
+
+
+
+ {:else if 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}
+
+
+
+
+ | Name |
+ Surname |
+ Email |
+ Scanned |
+ Email Sent |
+
+
+
+ {#each participants as participant}
+
+ | {participant.name} |
+ {participant.surname} |
+ {participant.email} |
+
+ {#if participant.scanned}
+
+ {:else}
+
+ {/if}
+ |
+
+ {#if participant.email_sent}
+
+ {:else}
+
+ {/if}
+ |
+
+ {/each}
+
+
+
+ {: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 @@
+
+
+
+
+
+
+ | Category |
+ Count |
+
+
+
+
+
+ |
+
+
+ Total participants
+
+ |
+
+ {#if loading}
+
+ {:else}
+ {totalCount}
+ {/if}
+ |
+
+
+
+
+
+
+
+ Scanned participants
+
+ |
+
+ {#if loading}
+
+ {:else}
+ {scannedCount}
+ {/if}
+ |
+
+
+
+
+ |
+
+ |
+
+ {#if loading}
+
+ {:else}
+ {emailSentCount}
+ {/if}
+ |
+
+
+
+
+ |
+
+ |
+
+ {#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}
+
+
+
This QR code is not a valid ticket or doesn't exist in our system.
+
+
+
Placeholder
+
Placeholder
+
-
This ticket belongs to a different event:
-
-
{ticket_data.event.name}
-
{ticket_data.name} {ticket_data.surname}
+ {:else if scan_state === ScanState.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}
+
+
+
Ticket successfully validated.
+
+
{ticket_data.event?.name || ''}
+
{ticket_data.name || ''} {ticket_data.surname || ''}
+
-
- {:else if scan_state === ScanState.scan_successful}
-
-
-
-
{ticket_data.event.name}
-
{ticket_data.name} {ticket_data.surname}
-
-
- {/if}
+ {/if}
+