// Application form API endpoint // Handles full event applications with PostgreSQL storage and Resend emails const { Pool } = require('pg'); const { Resend } = require('resend'); // Initialize PostgreSQL connection pool const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false }); // Initialize Resend const resend = new Resend(process.env.RESEND_API_KEY); // Email templates const confirmationEmail = (application) => ({ subject: 'Application Received - Valley of the Commons', html: `

Thank You for Applying!

Dear ${application.first_name},

We've received your application to join Valley of the Commons (August 24 - September 20, 2026).

What happens next?

  1. Our team will review your application
  2. We may reach out with follow-up questions
  3. You'll receive a decision within 2-3 weeks

In the meantime, feel free to explore more about the Commons Hub and our community:

If you have any questions, reply to this email and we'll get back to you.

With warmth,
The Valley of the Commons Team


Application ID: ${application.id}
Submitted: ${new Date(application.submitted_at).toLocaleDateString('en-US', { dateStyle: 'long' })}

` }); const adminNotificationEmail = (application) => ({ subject: `New Application: ${application.first_name} ${application.last_name}`, html: `

New Application Received

Name: ${application.first_name} ${application.last_name}
Email: ${application.email}
Location: ${application.city || ''}, ${application.country || ''}
Attendance: ${application.attendance_type === 'full' ? 'Full 4 weeks' : 'Partial'}
Scholarship: ${application.scholarship_needed ? 'Yes' : 'No'}

Motivation

${application.motivation}

Review Application

Application ID: ${application.id}

` }); async function logEmail(recipientEmail, recipientName, emailType, subject, resendId, metadata = {}) { try { await pool.query( `INSERT INTO email_log (recipient_email, recipient_name, email_type, subject, resend_id, metadata) VALUES ($1, $2, $3, $4, $5, $6)`, [recipientEmail, recipientName, emailType, subject, resendId, JSON.stringify(metadata)] ); } catch (error) { console.error('Failed to log email:', error); } } module.exports = async function handler(req, res) { // CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { return res.status(200).end(); } // POST - Submit new application if (req.method === 'POST') { try { const data = req.body; // Validate required fields const required = ['first_name', 'last_name', 'email', 'motivation', 'code_of_conduct_accepted', 'privacy_policy_accepted']; for (const field of required) { if (!data[field]) { return res.status(400).json({ error: `Missing required field: ${field}` }); } } // Validate email format if (!data.email.includes('@')) { return res.status(400).json({ error: 'Invalid email address' }); } // Check for duplicate application const existing = await pool.query( 'SELECT id FROM applications WHERE email = $1', [data.email.toLowerCase().trim()] ); if (existing.rows.length > 0) { return res.status(409).json({ error: 'An application with this email already exists', applicationId: existing.rows[0].id }); } // Prepare arrays for PostgreSQL const skills = Array.isArray(data.skills) ? data.skills : (data.skills ? [data.skills] : null); const languages = Array.isArray(data.languages) ? data.languages : (data.languages ? [data.languages] : null); const dietary = Array.isArray(data.dietary_requirements) ? data.dietary_requirements : (data.dietary_requirements ? [data.dietary_requirements] : null); const governance = Array.isArray(data.governance_interest) ? data.governance_interest : (data.governance_interest ? [data.governance_interest] : null); const previousEvents = Array.isArray(data.previous_events) ? data.previous_events : (data.previous_events ? [data.previous_events] : null); // Insert application const result = await pool.query( `INSERT INTO applications ( first_name, last_name, email, phone, country, city, pronouns, date_of_birth, occupation, organization, skills, languages, website, social_links, attendance_type, arrival_date, departure_date, accommodation_preference, dietary_requirements, dietary_notes, motivation, contribution, projects, workshops_offer, commons_experience, community_experience, governance_interest, how_heard, referral_name, previous_events, emergency_name, emergency_phone, emergency_relationship, code_of_conduct_accepted, privacy_policy_accepted, photo_consent, scholarship_needed, scholarship_reason, contribution_amount, ip_address, user_agent ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41 ) RETURNING id, submitted_at`, [ data.first_name?.trim(), data.last_name?.trim(), data.email?.toLowerCase().trim(), data.phone?.trim() || null, data.country?.trim() || null, data.city?.trim() || null, data.pronouns?.trim() || null, data.date_of_birth || null, data.occupation?.trim() || null, data.organization?.trim() || null, skills, languages, data.website?.trim() || null, data.social_links ? JSON.stringify(data.social_links) : null, data.attendance_type || 'full', data.arrival_date || null, data.departure_date || null, data.accommodation_preference || null, dietary, data.dietary_notes?.trim() || null, data.motivation?.trim(), data.contribution?.trim() || null, data.projects?.trim() || null, data.workshops_offer?.trim() || null, data.commons_experience?.trim() || null, data.community_experience?.trim() || null, governance, data.how_heard?.trim() || null, data.referral_name?.trim() || null, previousEvents, data.emergency_name?.trim() || null, data.emergency_phone?.trim() || null, data.emergency_relationship?.trim() || null, data.code_of_conduct_accepted || false, data.privacy_policy_accepted || false, data.photo_consent || false, data.scholarship_needed || false, data.scholarship_reason?.trim() || null, data.contribution_amount || null, req.headers['x-forwarded-for'] || req.connection?.remoteAddress || null, req.headers['user-agent'] || null ] ); const application = { id: result.rows[0].id, submitted_at: result.rows[0].submitted_at, first_name: data.first_name, last_name: data.last_name, email: data.email, city: data.city, country: data.country, attendance_type: data.attendance_type, scholarship_needed: data.scholarship_needed, motivation: data.motivation }; // Send confirmation email to applicant if (process.env.RESEND_API_KEY) { try { const confirmEmail = confirmationEmail(application); const { data: emailData } = await resend.emails.send({ from: process.env.EMAIL_FROM || 'Valley of the Commons ', to: application.email, subject: confirmEmail.subject, html: confirmEmail.html }); await logEmail(application.email, `${application.first_name} ${application.last_name}`, 'application_confirmation', confirmEmail.subject, emailData?.id, { applicationId: application.id }); } catch (emailError) { console.error('Failed to send confirmation email:', emailError); } // Send notification to admins try { const adminEmail = adminNotificationEmail(application); const adminRecipients = (process.env.ADMIN_EMAILS || 'jeff@jeffemmett.com').split(','); const { data: emailData } = await resend.emails.send({ from: process.env.EMAIL_FROM || 'Valley of the Commons ', to: adminRecipients, subject: adminEmail.subject, html: adminEmail.html }); await logEmail(adminRecipients[0], 'Admin', 'admin_notification', adminEmail.subject, emailData?.id, { applicationId: application.id }); } catch (emailError) { console.error('Failed to send admin notification:', emailError); } } return res.status(201).json({ success: true, message: 'Application submitted successfully', applicationId: application.id }); } catch (error) { console.error('Application submission error:', error); return res.status(500).json({ error: 'Failed to submit application. Please try again.' }); } } // GET - Retrieve applications (admin only) if (req.method === 'GET') { // Simple token-based auth for admin access const authHeader = req.headers.authorization; if (!authHeader || authHeader !== `Bearer ${process.env.ADMIN_API_KEY}`) { return res.status(401).json({ error: 'Unauthorized' }); } try { const { status, limit = 50, offset = 0 } = req.query; let query = 'SELECT * FROM applications'; const params = []; if (status) { query += ' WHERE status = $1'; params.push(status); } query += ` ORDER BY submitted_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`; params.push(parseInt(limit), parseInt(offset)); const result = await pool.query(query, params); // Get total count let countQuery = 'SELECT COUNT(*) FROM applications'; if (status) { countQuery += ' WHERE status = $1'; } const countResult = await pool.query(countQuery, status ? [status] : []); return res.status(200).json({ applications: result.rows, total: parseInt(countResult.rows[0].count), limit: parseInt(limit), offset: parseInt(offset) }); } catch (error) { console.error('Failed to fetch applications:', error); return res.status(500).json({ error: 'Failed to fetch applications' }); } } return res.status(405).json({ error: 'Method not allowed' }); };