diff --git a/package-lock.json b/package-lock.json index 7bf4e88..229e9d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@supabase/supabase-js": "^2.50.0", "@sveltejs/adapter-node": "^5.2.12", "googleapis": "^150.0.1", - "papaparse": "^5.5.3" + "papaparse": "^5.5.3", + "quoted-printable": "^1.0.1" }, "devDependencies": { "@sveltejs/kit": "^2.16.0", @@ -2691,6 +2692,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quoted-printable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/quoted-printable/-/quoted-printable-1.0.1.tgz", + "integrity": "sha512-cihC68OcGiQOjGiXuo5Jk6XHANTHl1K4JLk/xlEJRTIXfy19Sg6XzB95XonYgr+1rB88bCpr7WZE7D7AlZow4g==", + "license": "MIT", + "dependencies": { + "utf8": "^2.1.0" + }, + "bin": { + "quoted-printable": "bin/quoted-printable" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3050,6 +3063,12 @@ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", "license": "BSD" }, + "node_modules/utf8": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz", + "integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index d856c29..1530aa6 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@supabase/supabase-js": "^2.50.0", "@sveltejs/adapter-node": "^5.2.12", "googleapis": "^150.0.1", - "papaparse": "^5.5.3" + "papaparse": "^5.5.3", + "quoted-printable": "^1.0.1" } } diff --git a/src/lib/google.ts b/src/lib/google.ts index 905f680..c62e915 100644 --- a/src/lib/google.ts +++ b/src/lib/google.ts @@ -1,5 +1,6 @@ import { google } from 'googleapis'; import { env } from '$env/dynamic/private'; +import quotedPrintable from 'quoted-printable'; // tiny, zero-dep package export const scopes = ['https://www.googleapis.com/auth/gmail.send']; @@ -27,21 +28,73 @@ export async function exchangeCodeForTokens(code: string) { export async function sendGmail( refreshToken: string, - { to, subject, text }: { to: string; subject: string; text: 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 raw = Buffer - .from( - [`To: ${to}`, - 'Content-Type: text/plain; charset="UTF-8"', - 'Content-Transfer-Encoding: 7bit', - `Subject: ${subject}`, - '', - text].join('\n')) - .toString('base64url'); + + const message_html = + ` + + + + + +
+

${text}

+ QR Code +
+
+
+
+
+
+
+
+

This email has been generated with the help of *insert software name*

+
+
+ +`; + + + const boundary = 'BOUNDARY'; + const nl = '\r\n'; // RFC-5322 line ending + + const htmlQP = quotedPrintable.encode(message_html); + 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', diff --git a/src/routes/private/+layout.svelte b/src/routes/private/+layout.svelte index 60352a1..62b9f4c 100644 --- a/src/routes/private/+layout.svelte +++ b/src/routes/private/+layout.svelte @@ -11,6 +11,6 @@ -
+
diff --git a/src/routes/private/api/gmail/+server.ts b/src/routes/private/api/gmail/+server.ts index 8cd7ce3..e0ab49a 100644 --- a/src/routes/private/api/gmail/+server.ts +++ b/src/routes/private/api/gmail/+server.ts @@ -36,13 +36,13 @@ export const GET: RequestHandler = async ({ url }) => { /* ───────────── POST ───────────── */ export const POST: RequestHandler = async ({ request }) => { - const { action, refreshToken, to, subject, text } = await request.json(); + const { action, refreshToken, to, subject, text, qr_code } = await request.json(); /* send e-mail */ if (action === 'send') { if (!refreshToken) return new Response('Missing token', { status: 401 }); try { - await sendGmail(refreshToken, { to, subject, text }); + await sendGmail(refreshToken, { to, subject, text, qr_code }); return json({ ok: true }); } catch (err) { return new Response((err as Error).message, { status: 500 }); diff --git a/src/routes/private/creator/+page.server.ts b/src/routes/private/creator/+page.server.ts index 6c5b7c0..869e800 100644 --- a/src/routes/private/creator/+page.server.ts +++ b/src/routes/private/creator/+page.server.ts @@ -4,8 +4,6 @@ import Papa from 'papaparse'; import { fail } from '@sveltejs/kit'; export async function load({ locals }) { - console.log("▶️ fetching events…"); - const { data: events, error } = await locals.supabase .from('events') .select('*') @@ -23,8 +21,6 @@ export async function load({ locals }) { export const actions = { create: async (event) => { const formData = await event.request.formData(); - console.log(Array.from(formData.entries())); - console.log('create_event date', formData.get("date"), formData.get("name"), formData.get("description")); let { data: new_event, error } = await event.locals.supabase.rpc("create_event", { "p_name": formData.get('name'), @@ -51,8 +47,6 @@ export const actions = { parsedRows.shift(); } - console.log('Parsed rows:', parsedRows); - // Map each row to an object with keys: name, surname, email const participants = parsedRows.map((row: string[]) => ({ name: row[0], @@ -60,8 +54,6 @@ export const actions = { email: row[2] })); - console.log('Mapped participants:', participants); - return { participants, } diff --git a/src/routes/private/creator/+page.svelte b/src/routes/private/creator/+page.svelte index af11e51..4c1f3b1 100644 --- a/src/routes/private/creator/+page.svelte +++ b/src/routes/private/creator/+page.svelte @@ -11,9 +11,9 @@ let participants = $state([]); let subject = $state(''); let body = $state(''); + let authorized = $state(false); - // Update events and participants from the form data - $effect( () => { + $effect(() => { if (form && form.new_event) { new_event = form.new_event; } @@ -31,36 +31,26 @@ StepOverview ]; - // State variable for current step - let step = $state(0); + let step: number = $state(0); + + let stepConditions = $derived([ + authorized, + !!new_event?.name, + !!participants?.length, + !!subject && !!body + ]); - // Helper to go to next/previous step (optional) function nextStep() { if (step < steps.length - 1) step += 1; - console.log(step); } function prevStep() { if (step > 0) step -= 1; - console.log(step); } - -{#if step == 0} - -{:else if step == 1} - -{:else if step == 2} - -{:else if step == 3} - -{:else if step == 4} - -{/if} - -
+
+ +{#if step == 0} + +{:else if step == 1} + +{:else if step == 2} + +{:else if step == 3} + +{:else if step == 4} + +{/if} diff --git a/src/routes/private/creator/emailtest/+page.svelte b/src/routes/private/creator/emailtest/+page.svelte new file mode 100644 index 0000000..b6cc634 --- /dev/null +++ b/src/routes/private/creator/emailtest/+page.svelte @@ -0,0 +1,110 @@ + + +
+

Test Email Sender

+ {#if !authorized} +
+

Google not connected.

+ +
+ {:else} +
+ + + + + +
+ {/if} + {#if error} +
{error}
+ {/if} + {#if success} +
{success}
+ {/if} +
diff --git a/src/routes/private/creator/steps/StepConnectGoogle.svelte b/src/routes/private/creator/steps/StepConnectGoogle.svelte index bc663c5..77d8f99 100644 --- a/src/routes/private/creator/steps/StepConnectGoogle.svelte +++ b/src/routes/private/creator/steps/StepConnectGoogle.svelte @@ -2,8 +2,10 @@ import { onMount } from 'svelte'; import { goto } from '$app/navigation'; + export let authorized = false; + let refreshToken = ''; - let authorized = false; + let loading = true; let to = ''; let subject = ''; @@ -22,30 +24,15 @@ } onMount(async () => { - console.log("on mount"); refreshToken = localStorage.getItem('gmail_refresh_token') ?? ''; + loading = true; authorized = await validateToken(refreshToken); + loading = false; }); /* ⇢ redirects straight to Google via server 302 */ const connect = () => goto('/private/api/gmail?action=auth'); - async function sendEmail() { - const r = await fetch('/private/api/gmail', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - action: 'send', - to, - subject, - text: body, - refreshToken - }) - }); - r.ok ? alert('Sent!') : alert(await r.text()); - to = subject = body = ''; - } - async function disconnect() { if (!confirm('Disconnect Google account?')) return; await fetch('/private/api/gmail', { @@ -59,11 +46,30 @@ } -{#if !authorized} -
-

You haven’t connected your Google account yet.

- -
-{:else} - Your connection is good, proceed to next step -{/if} +
+ {#if loading} +
+ + + + + Checking Google connection... +
+ {:else} + {#if !authorized} +
+

You haven’t connected your Google account yet.

+ +
+ {:else} +
+ + + + Your connection to Google is good, proceed to next step +
+ {/if} + {/if} +
diff --git a/src/routes/private/creator/steps/StepCraftEmail.svelte b/src/routes/private/creator/steps/StepCraftEmail.svelte index 6e7956d..6f5da17 100644 --- a/src/routes/private/creator/steps/StepCraftEmail.svelte +++ b/src/routes/private/creator/steps/StepCraftEmail.svelte @@ -1,6 +1,6 @@
diff --git a/src/routes/private/creator/steps/StepCreateEvent.svelte b/src/routes/private/creator/steps/StepCreateEvent.svelte index 49d22e2..f3ca66d 100644 --- a/src/routes/private/creator/steps/StepCreateEvent.svelte +++ b/src/routes/private/creator/steps/StepCreateEvent.svelte @@ -2,11 +2,20 @@ import { enhance } from '$app/forms'; let { events, new_event } = $props(); + let loading = $state(false); + function handleEnhance() { + loading = true; + + return async ({ update }) => { + await update(); + loading = false; + }; + } - +

Create Event