From 196ca4589d574a23280a445f41108893d95e8de6 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 22 Jan 2026 14:35:15 +0100 Subject: [PATCH] Add Google Sheets and Resend email integration - Registration data sent to Google Sheet (all fields) - Confirmation email sent via Resend - Beautiful HTML email template matching site design - Both integrations are optional (gracefully disabled if not configured) Environment variables needed: - RESEND_API_KEY: Resend API key for emails - GOOGLE_SHEET_ID: Google Sheet ID for registrations - GOOGLE_CREDENTIALS: Service account JSON credentials Co-Authored-By: Claude Opus 4.5 --- docker-compose.yml | 3 + package.json | 4 +- server.js | 206 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 211 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8447540..2e73de5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,9 @@ services: - PORT=3000 - DATA_DIR=/app/data - ADMIN_TOKEN=${ADMIN_TOKEN:-worldplay-admin-2026} + - RESEND_API_KEY=${RESEND_API_KEY} + - GOOGLE_SHEET_ID=${GOOGLE_SHEET_ID} + - GOOGLE_CREDENTIALS=${GOOGLE_CREDENTIALS} volumes: - worldplay-data:/app/data labels: diff --git a/package.json b/package.json index 110ea25..409afc6 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "author": "WORLDPLAY Collective", "license": "MIT", "dependencies": { - "express": "^4.18.2" + "express": "^4.18.2", + "googleapis": "^144.0.0", + "resend": "^4.0.0" }, "engines": { "node": ">=18.0.0" diff --git a/server.js b/server.js index a4d9ca2..961ecdc 100644 --- a/server.js +++ b/server.js @@ -1,12 +1,32 @@ const express = require('express'); const fs = require('fs').promises; const path = require('path'); +const { google } = require('googleapis'); +const { Resend } = require('resend'); const app = express(); const PORT = process.env.PORT || 3000; const DATA_DIR = process.env.DATA_DIR || './data'; const REGISTRATIONS_FILE = path.join(DATA_DIR, 'registrations.json'); +// Initialize Resend for emails +const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null; + +// Google Sheets configuration +const GOOGLE_SHEET_ID = process.env.GOOGLE_SHEET_ID; +const GOOGLE_CREDENTIALS = process.env.GOOGLE_CREDENTIALS ? JSON.parse(process.env.GOOGLE_CREDENTIALS) : null; + +// Initialize Google Sheets API +let sheets = null; +if (GOOGLE_CREDENTIALS && GOOGLE_SHEET_ID) { + const auth = new google.auth.GoogleAuth({ + credentials: GOOGLE_CREDENTIALS, + scopes: ['https://www.googleapis.com/auth/spreadsheets'], + }); + sheets = google.sheets({ version: 'v4', auth }); + console.log('Google Sheets integration enabled'); +} + // Middleware app.use(express.json()); app.use(express.static('.')); @@ -40,6 +60,182 @@ async function saveRegistrations(registrations) { await fs.writeFile(REGISTRATIONS_FILE, JSON.stringify(registrations, null, 2)); } +// Append registration to Google Sheet +async function appendToGoogleSheet(registration) { + if (!sheets || !GOOGLE_SHEET_ID) { + console.log('Google Sheets not configured, skipping...'); + return; + } + + try { + const values = [[ + registration.registeredAt, + registration.firstName, + registration.lastName, + registration.email, + registration.location, + registration.role, + Array.isArray(registration.interests) ? registration.interests.join(', ') : registration.interests, + registration.contribute, + registration.message, + registration.id + ]]; + + await sheets.spreadsheets.values.append({ + spreadsheetId: GOOGLE_SHEET_ID, + range: 'Registrations!A:J', + valueInputOption: 'USER_ENTERED', + insertDataOption: 'INSERT_ROWS', + requestBody: { values }, + }); + + console.log(`Added registration to Google Sheet: ${registration.email}`); + } catch (error) { + console.error('Error appending to Google Sheet:', error.message); + } +} + +// Send confirmation email +async function sendConfirmationEmail(registration) { + if (!resend) { + console.log('Resend not configured, skipping email...'); + return; + } + + try { + const interestsText = Array.isArray(registration.interests) && registration.interests.length > 0 + ? registration.interests.map(i => { + const labels = { + 'reality': '🎭 Playing with Reality', + 'fiction': '✒️ Science Fictions', + 'worlding': '🛠 Guerrilla Futuring', + 'games': '🎲 Game Commons', + 'infrastructure': '🌱 Infrastructures' + }; + return labels[i] || i; + }).join(', ') + : 'Not specified'; + + const roleLabels = { + 'writer': 'Sci-Fi Writer / Storyteller', + 'gamemaker': 'Game Designer / Maker', + 'artist': 'Artist / Performer', + 'larper': 'LARPer / Roleplayer', + 'economist': 'Weird Economist / Commons Activist', + 'futurist': 'Futurist / Speculative Designer', + 'academic': 'Academic / Researcher', + 'tech': 'Technologist / Developer', + 'curious': 'Curious Explorer', + 'other': 'Other' + }; + + const contributeLabels = { + 'session': 'Pitch a session', + 'workshop': 'Run a workshop', + 'game': 'Share/playtest a game', + 'performance': 'Perform/facilitate', + 'collaborate': 'Collaborate on something', + 'participate': 'Participate', + 'unsure': 'Not sure yet' + }; + + await resend.emails.send({ + from: 'WORLDPLAY ', + to: registration.email, + subject: '🎭 Welcome to WORLDPLAY – Registration Confirmed', + html: ` + + + + + + + +
+
+

WORLDPLAY

+

: To be Defined

+
+ +
+

Welcome, ${registration.firstName}! 🌟

+ +

+ Thank you for registering your interest in WORLDPLAY – a pop-up physical hub for prefiguring and prehearsing postcapitalist futures through fiction, performance and play. +

+ +

+ We've received your registration and will be in touch with next steps as we finalize the programme. +

+
+ +
+

Your Registration Details

+ + + + + + + + + + + ${registration.location ? ` + + + + + ` : ''} + ${registration.role ? ` + + + + + ` : ''} + + + + + ${registration.contribute ? ` + + + + + ` : ''} +
Name:${registration.firstName} ${registration.lastName}
Email:${registration.email}
Location:${registration.location}
Role:${roleLabels[registration.role] || registration.role}
Interests:${interestsText}
Contribution:${contributeLabels[registration.contribute] || registration.contribute}
+
+ +
+

Event Details

+ +

+ 📅 June 7–13, 2026
+ 📍 Commons Hub, Hirschwang an der Rax, Austria
+ 🏔️ Austrian Alps, ~1.5 hours from Vienna by train +

+
+ +
+

+ Questions? Reply to this email or visit worldplay.art +

+

+ Reality is a design space ✨ +

+
+
+ + + `, + }); + + console.log(`Confirmation email sent to: ${registration.email}`); + } catch (error) { + console.error('Error sending confirmation email:', error.message); + } +} + // Registration endpoint app.post('/api/register', async (req, res) => { try { @@ -77,12 +273,18 @@ app.post('/api/register', async (req, res) => { ipAddress: req.ip || req.connection.remoteAddress }; - // Save registration + // Save registration locally registrations.push(registration); await saveRegistrations(registrations); console.log(`New registration: ${registration.firstName} ${registration.lastName} <${registration.email}>`); + // Add to Google Sheet (async, don't block response) + appendToGoogleSheet(registration).catch(err => console.error('Sheet error:', err)); + + // Send confirmation email (async, don't block response) + sendConfirmationEmail(registration).catch(err => console.error('Email error:', err)); + res.json({ success: true, message: 'Registration successful', @@ -204,5 +406,7 @@ ensureDataDir().then(() => { app.listen(PORT, '0.0.0.0', () => { console.log(`WORLDPLAY server running on port ${PORT}`); console.log(`Admin token: ${process.env.ADMIN_TOKEN || 'worldplay-admin-2026'}`); + console.log(`Google Sheets: ${sheets ? 'enabled' : 'disabled'}`); + console.log(`Email notifications: ${resend ? 'enabled' : 'disabled'}`); }); });