diff --git a/.env.example b/.env.example
index e3df937..32ce1d1 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,7 @@
PUBLIC_SUPABASE_URL=https://abc.supabase.co
-PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI16C_s
\ No newline at end of file
+PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI16C_s
+
+# Google OAuth Configuration
+GOOGLE_CLIENT_ID=your-google-client-id
+GOOGLE_CLIENT_SECRET=your-google-client-secret
+GOOGLE_REDIRECT_URI=http://localhost:5173
\ No newline at end of file
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index c53c216..a67c7e3 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -85,4 +85,85 @@ NEVER $: label syntax; use $state(), $derived(), and $effect().
If you want to use supabse client in the browser, it is stored in the data
variable obtained from let { data } = $props();
-Using `on:click` to listen to the click event is deprecated. Use the event attribute `onclick` instead
\ No newline at end of file
+Using `on:click` to listen to the click event is deprecated. Use the event attribute `onclick` instead
+
+onsubmit|preventDefault={handleSubmit} is depracated, do not use it!
+
+Loading session using page.server.ts is not needed as the session is already available in the locals object.
+
+
+The database schema in supabase is as follows:
+-- WARNING: This schema is for context only and is not meant to be run.
+-- Table order and constraints may not be valid for execution.
+
+CREATE TABLE public.events (
+ id uuid NOT NULL DEFAULT gen_random_uuid(),
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
+ created_by uuid DEFAULT auth.uid(),
+ name text,
+ date date,
+ section_id uuid,
+ email_subject text,
+ email_body text,
+ sheet_id text,
+ name_column numeric,
+ surname_column numeric,
+ email_column numeric,
+ confirmation_column numeric,
+ CONSTRAINT events_pkey PRIMARY KEY (id),
+ CONSTRAINT events_created_by_fkey FOREIGN KEY (created_by) REFERENCES auth.users(id),
+ CONSTRAINT events_section_id_fkey FOREIGN KEY (section_id) REFERENCES public.sections(id)
+);
+CREATE TABLE public.events_archived (
+ id uuid NOT NULL DEFAULT gen_random_uuid(),
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
+ date date,
+ name text NOT NULL,
+ total_participants numeric,
+ scanned_participants numeric,
+ section_id uuid,
+ CONSTRAINT events_archived_pkey PRIMARY KEY (id),
+ CONSTRAINT events_archived_section_id_fkey FOREIGN KEY (section_id) REFERENCES public.sections(id)
+);
+CREATE TABLE public.participants (
+ id uuid NOT NULL DEFAULT gen_random_uuid(),
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
+ created_by uuid DEFAULT auth.uid(),
+ event uuid,
+ name text,
+ surname text,
+ email text,
+ scanned boolean DEFAULT false,
+ scanned_at timestamp with time zone,
+ scanned_by uuid,
+ section_id uuid,
+ CONSTRAINT participants_pkey PRIMARY KEY (id),
+ CONSTRAINT participants_created_by_fkey FOREIGN KEY (created_by) REFERENCES auth.users(id),
+ CONSTRAINT participants_event_fkey FOREIGN KEY (event) REFERENCES public.events(id),
+ CONSTRAINT participants_scanned_by_fkey FOREIGN KEY (scanned_by) REFERENCES public.profiles(id),
+ CONSTRAINT qrcodes_scanned_by_fkey FOREIGN KEY (scanned_by) REFERENCES auth.users(id),
+ CONSTRAINT qrcodes_section_id_fkey FOREIGN KEY (section_id) REFERENCES public.sections(id)
+);
+CREATE TABLE public.profiles (
+ id uuid NOT NULL,
+ display_name text,
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ section_id uuid,
+ section_position USER-DEFINED NOT NULL DEFAULT 'member'::section_posititon,
+ CONSTRAINT profiles_pkey PRIMARY KEY (id),
+ CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id),
+ CONSTRAINT profiles_section_id_fkey FOREIGN KEY (section_id) REFERENCES public.sections(id)
+);
+CREATE TABLE public.sections (
+ id uuid NOT NULL DEFAULT gen_random_uuid(),
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
+ name text NOT NULL UNIQUE,
+ CONSTRAINT sections_pkey PRIMARY KEY (id)
+);
+
+An event is created by calling RPC databse function create_event
+by passing the following parameters:
+- name, date, email_subject, email_body, sheet_id, name_column, surname_column, email_column, confirmation_column
+
+
diff --git a/src/lib/gmail.ts b/src/lib/gmail.ts
new file mode 100644
index 0000000..c46488d
--- /dev/null
+++ b/src/lib/gmail.ts
@@ -0,0 +1,80 @@
+import { google } from 'googleapis';
+import quotedPrintable from 'quoted-printable';
+import { getAuthenticatedClient } from './google-server.js';
+
+export function createEmailTemplate(text: string): string {
+ return `
+
+
+
+
+
+
+
${text}
+
+
+
+
This email has been generated with the help of ScanWave
+
+
+
+`;
+}
+
+export async function sendGmail(
+ refreshToken: string,
+ { to, subject, text, qr_code }: { to: string; subject: string; text: string; qr_code: string }
+) {
+ const oauth = getAuthenticatedClient(refreshToken);
+ const gmail = google.gmail({ version: 'v1', auth: oauth });
+
+ const message_html = createEmailTemplate(text);
+ const boundary = 'BOUNDARY';
+ const nl = '\r\n';
+
+ // Convert HTML to a Buffer, then to latin1 string for quotedPrintable.encode
+ const htmlBuffer = Buffer.from(message_html, 'utf8');
+ const htmlLatin1 = htmlBuffer.toString('latin1');
+ const htmlQP = quotedPrintable.encode(htmlLatin1);
+ const qrLines = qr_code.replace(/.{1,76}/g, '$&' + nl);
+
+ const rawParts = [
+ 'MIME-Version: 1.0',
+ `To: ${to}`,
+ `Subject: ${subject}`,
+ `Content-Type: multipart/related; boundary="${boundary}"`,
+ '',
+ `--${boundary}`,
+ 'Content-Type: text/html; charset="UTF-8"',
+ 'Content-Transfer-Encoding: quoted-printable',
+ '',
+ htmlQP,
+ '',
+ `--${boundary}`,
+ 'Content-Type: image/png',
+ 'Content-Transfer-Encoding: base64',
+ 'Content-ID: ',
+ 'Content-Disposition: inline; filename="qr.png"',
+ '',
+ qrLines,
+ '',
+ `--${boundary}--`,
+ ''
+ ];
+
+ const rawMessage = rawParts.join(nl);
+ const raw = Buffer.from(rawMessage).toString('base64url');
+
+ await gmail.users.messages.send({
+ userId: 'me',
+ requestBody: { raw }
+ });
+}
diff --git a/src/lib/google-server.ts b/src/lib/google-server.ts
new file mode 100644
index 0000000..7d98157
--- /dev/null
+++ b/src/lib/google-server.ts
@@ -0,0 +1,38 @@
+import { google } from 'googleapis';
+import { env } from '$env/dynamic/private';
+
+export const scopes = [
+ 'https://www.googleapis.com/auth/gmail.send',
+ 'https://www.googleapis.com/auth/userinfo.email',
+ 'https://www.googleapis.com/auth/drive.readonly',
+ 'https://www.googleapis.com/auth/spreadsheets.readonly'
+];
+
+export function getOAuthClient() {
+ return new google.auth.OAuth2(
+ env.GOOGLE_CLIENT_ID,
+ env.GOOGLE_CLIENT_SECRET,
+ env.GOOGLE_REDIRECT_URI
+ );
+}
+
+export function createAuthUrl() {
+ return getOAuthClient().generateAuthUrl({
+ access_type: 'offline',
+ prompt: 'consent',
+ scope: scopes,
+ redirect_uri: env.GOOGLE_REDIRECT_URI
+ });
+}
+
+export async function exchangeCodeForTokens(code: string) {
+ const { tokens } = await getOAuthClient().getToken(code);
+ if (!tokens.refresh_token) throw new Error('No refresh_token returned');
+ return tokens.refresh_token;
+}
+
+export function getAuthenticatedClient(refreshToken: string) {
+ const oauth = getOAuthClient();
+ oauth.setCredentials({ refresh_token: refreshToken });
+ return oauth;
+}
diff --git a/src/lib/google.ts b/src/lib/google.ts
index 792deb2..718199b 100644
--- a/src/lib/google.ts
+++ b/src/lib/google.ts
@@ -1,108 +1,60 @@
-import { google } from 'googleapis';
-import { env } from '$env/dynamic/private';
-import quotedPrintable from 'quoted-printable'; // tiny, zero-dep package
+import { browser } from '$app/environment';
+// Client-side only functions
export const scopes = [
'https://www.googleapis.com/auth/gmail.send',
- 'https://www.googleapis.com/auth/userinfo.email'
+ 'https://www.googleapis.com/auth/userinfo.email',
+ 'https://www.googleapis.com/auth/drive.readonly',
+ 'https://www.googleapis.com/auth/spreadsheets.readonly'
];
-export function getOAuthClient() {
- return new google.auth.OAuth2(
- env.GOOGLE_CLIENT_ID,
- env.GOOGLE_CLIENT_SECRET,
- env.GOOGLE_REDIRECT_URI
- );
+// Client-side functions for browser environment
+export async function initGoogleAuth(): Promise {
+ if (!browser) return;
+ // Google Auth initialization is handled by the OAuth flow
+ // No initialization needed for our server-side approach
}
-export function createAuthUrl() {
- return getOAuthClient().generateAuthUrl({
- access_type: 'offline',
- prompt: 'consent',
- scope: scopes
- });
+export function getAuthUrl(): string {
+ if (!browser) return '';
+ // This should be obtained from the server
+ return '/auth/google';
}
-export async function exchangeCodeForTokens(code: string) {
- const { tokens } = await getOAuthClient().getToken(code);
- if (!tokens.refresh_token) throw new Error('No refresh_token returned');
- return tokens.refresh_token;
+export async function isTokenValid(accessToken: string): Promise {
+ if (!browser) return false;
+
+ try {
+ const response = await fetch(`https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${accessToken}`);
+ const data = await response.json();
+
+ if (response.ok && data.expires_in && data.expires_in > 0) {
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.error('Error validating token:', error);
+ return false;
+ }
}
-export async function sendGmail(
- refreshToken: string,
- { to, subject, text, qr_code }: { to: string; subject: string; text: string; qr_code: string }
-) {
- const oauth = getOAuthClient();
- oauth.setCredentials({ refresh_token: refreshToken });
-
- const gmail = google.gmail({ version: 'v1', auth: oauth });
-
- const message_html =
- `
-
-
-
-
-
-
-
${text}
-
-
-
-
This email has been generated with the help of ScanWave
-
-
-
-`;
-
- const boundary = 'BOUNDARY';
- const nl = '\r\n'; // RFC-5322 line ending
-
- // Convert HTML to a Buffer, then to latin1 string for quotedPrintable.encode
- const htmlBuffer = Buffer.from(message_html, 'utf8');
- const htmlLatin1 = htmlBuffer.toString('latin1');
- const htmlQP = quotedPrintable.encode(htmlLatin1);
- const qrLines = qr_code.replace(/.{1,76}/g, '$&' + nl);
-
- const rawParts = [
- 'MIME-Version: 1.0',
- `To: ${to}`,
- `Subject: ${subject}`,
- `Content-Type: multipart/related; boundary="${boundary}"`,
- '',
- `--${boundary}`,
- 'Content-Type: text/html; charset="UTF-8"',
- 'Content-Transfer-Encoding: quoted-printable',
- '',
- htmlQP,
- '',
- `--${boundary}`,
- 'Content-Type: image/png',
- 'Content-Transfer-Encoding: base64',
- 'Content-ID: ',
- 'Content-Disposition: inline; filename="qr.png"',
- '',
- qrLines,
- '',
- `--${boundary}--`,
- ''
- ];
-
- const rawMessage = rawParts.join(nl);
-
- const raw = Buffer.from(rawMessage).toString('base64url');
-
- await gmail.users.messages.send({
- userId: 'me',
- requestBody: { raw }
- });
+export async function refreshAccessToken(refreshToken: string): Promise {
+ try {
+ const response = await fetch('/api/auth/refresh', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ refreshToken })
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ return data.accessToken;
+ }
+ return null;
+ } catch (error) {
+ console.error('Error refreshing token:', error);
+ return null;
+ }
}
diff --git a/src/lib/sheets.ts b/src/lib/sheets.ts
new file mode 100644
index 0000000..b8dcc4c
--- /dev/null
+++ b/src/lib/sheets.ts
@@ -0,0 +1,58 @@
+import { google } from 'googleapis';
+import { getAuthenticatedClient } from './google-server.js';
+
+export interface GoogleSheet {
+ id: string;
+ name: string;
+ modifiedTime: string;
+ webViewLink: string;
+}
+
+export interface SheetData {
+ values: string[][];
+}
+
+export async function getRecentSpreadsheets(refreshToken: string, limit: number = 10): Promise {
+ const oauth = getAuthenticatedClient(refreshToken);
+ const drive = google.drive({ version: 'v3', auth: oauth });
+
+ const response = await drive.files.list({
+ q: "mimeType='application/vnd.google-apps.spreadsheet'",
+ orderBy: 'modifiedTime desc',
+ pageSize: limit,
+ fields: 'files(id,name,modifiedTime,webViewLink)'
+ });
+
+ return response.data.files?.map(file => ({
+ id: file.id!,
+ name: file.name!,
+ modifiedTime: file.modifiedTime!,
+ webViewLink: file.webViewLink!
+ })) || [];
+}
+
+export async function getSpreadsheetData(refreshToken: string, spreadsheetId: string, range: string = 'A1:Z10'): Promise {
+ const oauth = getAuthenticatedClient(refreshToken);
+ const sheets = google.sheets({ version: 'v4', auth: oauth });
+
+ const response = await sheets.spreadsheets.values.get({
+ spreadsheetId,
+ range
+ });
+
+ return {
+ values: response.data.values || []
+ };
+}
+
+export async function getSpreadsheetInfo(refreshToken: string, spreadsheetId: string) {
+ const oauth = getAuthenticatedClient(refreshToken);
+ const sheets = google.sheets({ version: 'v4', auth: oauth });
+
+ const response = await sheets.spreadsheets.get({
+ spreadsheetId,
+ fields: 'properties.title,sheets.properties(title,sheetId)'
+ });
+
+ return response.data;
+}
diff --git a/src/routes/api/auth/refresh/+server.ts b/src/routes/api/auth/refresh/+server.ts
new file mode 100644
index 0000000..e6561ee
--- /dev/null
+++ b/src/routes/api/auth/refresh/+server.ts
@@ -0,0 +1,30 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { getOAuthClient } from '$lib/google-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/sheets/[sheetId]/data/+server.ts b/src/routes/api/sheets/[sheetId]/data/+server.ts
new file mode 100644
index 0000000..263e099
--- /dev/null
+++ b/src/routes/api/sheets/[sheetId]/data/+server.ts
@@ -0,0 +1,22 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { getSpreadsheetData } from '$lib/sheets.js';
+
+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 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
new file mode 100644
index 0000000..d9b94a2
--- /dev/null
+++ b/src/routes/api/sheets/recent/+server.ts
@@ -0,0 +1,20 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { getRecentSpreadsheets } from '$lib/sheets.js';
+
+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 sheets = await getRecentSpreadsheets(refreshToken, 20);
+
+ return json(sheets);
+ } catch (error) {
+ console.error('Error fetching recent spreadsheets:', error);
+ return json({ error: 'Failed to fetch spreadsheets' }, { status: 500 });
+ }
+};
diff --git a/src/routes/auth/google/+server.ts b/src/routes/auth/google/+server.ts
new file mode 100644
index 0000000..d407c22
--- /dev/null
+++ b/src/routes/auth/google/+server.ts
@@ -0,0 +1,8 @@
+import { redirect } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { createAuthUrl } from '$lib/google-server.js';
+
+export const GET: RequestHandler = () => {
+ const authUrl = createAuthUrl();
+ throw redirect(302, authUrl);
+};
diff --git a/src/routes/auth/google/callback/+server.ts b/src/routes/auth/google/callback/+server.ts
new file mode 100644
index 0000000..840490c
--- /dev/null
+++ b/src/routes/auth/google/callback/+server.ts
@@ -0,0 +1,110 @@
+import { redirect } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { getOAuthClient } from '$lib/google-server.js';
+
+export const GET: RequestHandler = async ({ url }) => {
+ try {
+ const code = url.searchParams.get('code');
+ const error = url.searchParams.get('error');
+
+ if (error) {
+ console.error('Google OAuth error:', error);
+ throw redirect(302, '/private/events?error=google_auth_denied');
+ }
+
+ if (!code) {
+ throw redirect(302, '/private/events?error=missing_auth_code');
+ }
+
+ // Exchange code for tokens
+ const oauth = getOAuthClient();
+ const { tokens } = await oauth.getToken(code);
+
+ if (!tokens.refresh_token || !tokens.access_token) {
+ throw redirect(302, '/private/events?error=incomplete_tokens');
+ }
+
+ // Create a success page with tokens that closes the popup and communicates with parent
+ const html = `
+
+
+
+ Google Authentication Success
+
+
+
+
+
✓ Authentication successful!
+
Closing window...
+
+
+
+`;
+
+ return new Response(html, {
+ headers: {
+ 'Content-Type': 'text/html'
+ }
+ });
+ } catch (error) {
+ console.error('Error handling Google OAuth callback:', error);
+ throw redirect(302, '/private/events?error=google_auth_failed');
+ }
+};
diff --git a/src/routes/private/api/gmail/+server.ts b/src/routes/private/api/gmail/+server.ts
index ca0e7d4..62b73e8 100644
--- a/src/routes/private/api/gmail/+server.ts
+++ b/src/routes/private/api/gmail/+server.ts
@@ -3,9 +3,9 @@ import { json, redirect } from '@sveltejs/kit';
import {
createAuthUrl,
exchangeCodeForTokens,
- sendGmail,
getOAuthClient
} from '$lib/google';
+import { sendGmail } from '$lib/gmail';
/* ───────────── GET ───────────── */
export const GET: RequestHandler = async ({ url }) => {
diff --git a/src/routes/private/events/+page.svelte b/src/routes/private/events/+page.svelte
index 47dc1f6..01b0edc 100644
--- a/src/routes/private/events/+page.svelte
+++ b/src/routes/private/events/+page.svelte
@@ -1,53 +1,24 @@
All Events
- {#if loading}
- {#each Array(4) as _}
-
New Event
diff --git a/src/routes/private/events/event/new/+page.server.ts b/src/routes/private/events/event/new/+page.server.ts
new file mode 100644
index 0000000..036c394
--- /dev/null
+++ b/src/routes/private/events/event/new/+page.server.ts
@@ -0,0 +1,7 @@
+export const load = async ({ locals }: { locals: any }) => {
+ const { session } = await locals.safeGetSession();
+
+ return {
+ session
+ };
+};
diff --git a/src/routes/private/events/event/new/+page.svelte b/src/routes/private/events/event/new/+page.svelte
new file mode 100644
index 0000000..bad1c12
--- /dev/null
+++ b/src/routes/private/events/event/new/+page.svelte
@@ -0,0 +1,795 @@
+
+
+
+
+
+
Create New Event
+
+ {#each Array(totalSteps) as _, index}
+
+
+ {index + 1}
+
+ {#if index < totalSteps - 1}
+
+ {/if}
+
+ {/each}
+
+
Step {currentStep + 1} of {totalSteps}: {stepTitle}
+
+
+
+
+ {#if currentStep === 0}
+
+
+
+
Connect Your Google Account
+
+ To create events and import participants from Google Sheets, you need to connect your Google account.
+
+
+ {#if authData.checking}
+
+
+
Checking connection...
+
+ {:else if authData.isConnected}
+
+
+
+
+
+ Google account connected successfully!
+
+
+ You can now access Google Sheets and Gmail features.
+
+
+
+
+ {:else}
+
+
+
+
+
+ Google account not connected
+
+
+ Please connect your Google account to continue with event creation.
+
+
+
+
+
+
+
+ {#if authData.connecting}
+
+ Connecting...
+ {:else}
+
+
+
+
+
+
+ Connect to Google
+ {/if}
+
+
+ {#if authData.connecting && authData.showCancelOption}
+
+ Cancel connection
+
+
+ Taking too long? You can cancel and try again.
+
+ {/if}
+
+ {/if}
+
+ {#if authData.error}
+
+
+
+
+
+ Connection Error
+
+
+ {authData.error}
+
+
+
+
+ {/if}
+
+ {#if errors.auth}
+
{errors.auth}
+ {/if}
+
+
+
+ {:else if currentStep === 1}
+
+
+
+
+ Event Name *
+
+
+ {#if errors.name}
+
{errors.name}
+ {/if}
+
+
+
+
+ Event Date *
+
+
+ {#if errors.date}
+
{errors.date}
+ {/if}
+
+
+
+ {:else if currentStep === 2}
+
+
+
+
Select Google Sheet
+
+ {#if sheetsData.loading && sheetsData.availableSheets.length === 0}
+
+ {#each Array(5) as _}
+
+ {/each}
+
+ {:else if sheetsData.availableSheets.length === 0}
+
+
No Google Sheets found.
+
+ Refresh
+
+
+ {:else}
+
+ {#if !sheetsData.expandedSheetList && sheetsData.selectedSheet}
+
+
+
+
{sheetsData.selectedSheet.name}
+
+ Modified: {new Date(sheetsData.selectedSheet.modifiedTime).toLocaleDateString()}
+
+
+
+ Change
+
+
+
+
+
+ {:else}
+
+
+
Available Sheets
+ {#if sheetsData.selectedSheet}
+
+ Collapse list
+
+ {/if}
+
+
+ {#each sheetsData.availableSheets as sheet}
+
selectSheet(sheet)}
+ class="p-4 text-left border border-gray-200 rounded hover:border-blue-500 transition {
+ sheetsData.selectedSheet?.id === sheet.id ? 'border-blue-500 bg-blue-50' : ''
+ }"
+ >
+ {sheet.name}
+
+ Modified: {new Date(sheet.modifiedTime).toLocaleDateString()}
+
+
+ {/each}
+
+ {/if}
+
+ {/if}
+
+ {#if errors.sheet}
+
{errors.sheet}
+ {/if}
+
+
+ {#if sheetsData.selectedSheet && sheetsData.sheetData.length > 0}
+
+
Column Mapping
+
+
+
+
Column Mapping Instructions:
+
+ Select what each column represents by using the dropdown in each column header.
+ Make sure to assign Name, Surname, Email, and Confirmation columns.
+
+
+
+
+
+
+
+ {#each sheetsData.sheetData[0] || [] as header, index}
+
+
+
+ Column {index + 1}
+ ({header || 'Empty'})
+
+
e.stopPropagation()}
+ onchange={(e) => {
+ const value = e.target.value;
+ if (value === "none") return;
+
+ // Reset previous selection if this column was already mapped
+ if (sheetsData.columnMapping.name === index + 1) sheetsData.columnMapping.name = 0;
+ if (sheetsData.columnMapping.surname === index + 1) sheetsData.columnMapping.surname = 0;
+ if (sheetsData.columnMapping.email === index + 1) sheetsData.columnMapping.email = 0;
+ if (sheetsData.columnMapping.confirmation === index + 1) sheetsData.columnMapping.confirmation = 0;
+
+ // Set new mapping
+ if (value === "name") sheetsData.columnMapping.name = index + 1;
+ else if (value === "surname") sheetsData.columnMapping.surname = index + 1;
+ else if (value === "email") sheetsData.columnMapping.email = index + 1;
+ else if (value === "confirmation") sheetsData.columnMapping.confirmation = index + 1;
+ }}
+ >
+ Select data type
+ Name
+ Surname
+ Email
+ Confirmation
+
+ {#if sheetsData.columnMapping.name === index + 1}
+
Name Column
+ {:else if sheetsData.columnMapping.surname === index + 1}
+
Surname Column
+ {:else if sheetsData.columnMapping.email === index + 1}
+
Email Column
+ {:else if sheetsData.columnMapping.confirmation === index + 1}
+
Confirmation Column
+ {/if}
+
+
+ {/each}
+
+
+
+ {#each sheetsData.sheetData.slice(0, 10) as row, rowIndex}
+
+ {#each row as cell, cellIndex}
+
+
+ {cell || ''}
+
+
+ {/each}
+
+ {/each}
+
+
+
+
Showing first 10 rows
+
+ {/if}
+
+ {#if sheetsData.loading && sheetsData.selectedSheet}
+
+
Loading sheet data...
+
+ {/if}
+
+ {#if errors.sheetData}
+
{errors.sheetData}
+ {/if}
+
+
+ {:else if currentStep === 3}
+
+
+
+
+ Email Subject *
+
+
+ {#if errors.subject}
+
{errors.subject}
+ {/if}
+
+
+
+
+ Email Body *
+
+
+ {#if errors.body}
+
{errors.body}
+ {/if}
+
+
+
+
+
Preview
+
+
Subject: {emailData.subject || 'No subject'}
+
{emailData.body || 'No content'}
+
+
+
+ {/if}
+
+ {#if errors.submit}
+
+ {/if}
+
+
+
+
+
+ Previous
+
+
+
+ {#if currentStep < totalSteps - 1}
+
+ Next
+
+ {:else}
+
+ {loading ? 'Creating...' : 'Create Event'}
+
+ {/if}
+
+
+