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('.')); // Ensure data directory exists async function ensureDataDir() { try { await fs.mkdir(DATA_DIR, { recursive: true }); try { await fs.access(REGISTRATIONS_FILE); } catch { await fs.writeFile(REGISTRATIONS_FILE, '[]'); } } catch (error) { console.error('Error creating data directory:', error); } } // Load registrations async function loadRegistrations() { try { const data = await fs.readFile(REGISTRATIONS_FILE, 'utf8'); return JSON.parse(data); } catch { return []; } } // Save registrations 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 { const { firstName, lastName, email, location, role, interests, contribute, message } = req.body; // Validation if (!firstName || !lastName || !email) { return res.status(400).json({ error: 'First name, last name, and email are required' }); } if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { return res.status(400).json({ error: 'Please provide a valid email address' }); } // Load existing registrations const registrations = await loadRegistrations(); // Check for duplicate email if (registrations.some(r => r.email.toLowerCase() === email.toLowerCase())) { return res.status(400).json({ error: 'This email is already registered' }); } // Create new registration const registration = { id: Date.now().toString(36) + Math.random().toString(36).substr(2), firstName: firstName.trim(), lastName: lastName.trim(), email: email.toLowerCase().trim(), location: location?.trim() || '', role: role || '', interests: interests || [], contribute: contribute || '', message: message?.trim() || '', registeredAt: new Date().toISOString(), ipAddress: req.ip || req.connection.remoteAddress }; // 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', id: registration.id }); } catch (error) { console.error('Registration error:', error); res.status(500).json({ error: 'An error occurred. Please try again.' }); } }); // Admin endpoint to view registrations (protected by simple token) app.get('/api/registrations', async (req, res) => { const token = req.headers['x-admin-token'] || req.query.token; const adminToken = process.env.ADMIN_TOKEN || 'worldplay-admin-2026'; if (token !== adminToken) { return res.status(401).json({ error: 'Unauthorized' }); } try { const registrations = await loadRegistrations(); res.json({ count: registrations.length, registrations: registrations.map(r => ({ ...r, ipAddress: undefined // Don't expose IP in admin view })) }); } catch (error) { res.status(500).json({ error: 'Failed to load registrations' }); } }); // Export registrations as CSV app.get('/api/registrations/export', async (req, res) => { const token = req.headers['x-admin-token'] || req.query.token; const adminToken = process.env.ADMIN_TOKEN || 'worldplay-admin-2026'; if (token !== adminToken) { return res.status(401).json({ error: 'Unauthorized' }); } try { const registrations = await loadRegistrations(); const headers = ['ID', 'First Name', 'Last Name', 'Email', 'Location', 'Role', 'Interests', 'Contribute', 'Message', 'Registered At']; const rows = registrations.map(r => [ r.id, r.firstName, r.lastName, r.email, r.location, r.role, Array.isArray(r.interests) ? r.interests.join('; ') : r.interests, r.contribute, r.message.replace(/"/g, '""'), r.registeredAt ]); const csv = [ headers.join(','), ...rows.map(row => row.map(cell => `"${cell}"`).join(',')) ].join('\n'); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', 'attachment; filename=worldplay-registrations.csv'); res.send(csv); } catch (error) { res.status(500).json({ error: 'Failed to export registrations' }); } }); // Health check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Stats endpoint app.get('/api/stats', async (req, res) => { try { const registrations = await loadRegistrations(); const stats = { totalRegistrations: registrations.length, byRole: {}, byInterest: {}, byContribute: {} }; registrations.forEach(r => { // Count by role if (r.role) { stats.byRole[r.role] = (stats.byRole[r.role] || 0) + 1; } // Count by interest if (Array.isArray(r.interests)) { r.interests.forEach(interest => { stats.byInterest[interest] = (stats.byInterest[interest] || 0) + 1; }); } // Count by contribute if (r.contribute) { stats.byContribute[r.contribute] = (stats.byContribute[r.contribute] || 0) + 1; } }); res.json(stats); } catch (error) { res.status(500).json({ error: 'Failed to calculate stats' }); } }); // Start server 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'}`); }); });