feat: resume existing application by email lookup
Add GET /api/application/lookup endpoint for public email-based lookups, PUT /api/application handler for updating existing applications, and frontend flow that detects returning users after step 2 with a welcome-back modal to pre-fill and update their application. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0eafbb35a0
commit
b8823e32ec
|
|
@ -196,7 +196,7 @@ async function logEmail(recipientEmail, recipientName, emailType, subject, messa
|
||||||
module.exports = async function handler(req, res) {
|
module.exports = async function handler(req, res) {
|
||||||
// CORS headers
|
// CORS headers
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
|
|
@ -469,5 +469,241 @@ module.exports = async function handler(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PUT - Update existing application
|
||||||
|
if (req.method === 'PUT') {
|
||||||
|
try {
|
||||||
|
const data = req.body;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
const required = ['first_name', 'last_name', 'email', 'motivation', 'belief_update', 'privacy_policy_accepted'];
|
||||||
|
for (const field of required) {
|
||||||
|
if (!data[field]) {
|
||||||
|
return res.status(400).json({ error: `Missing required field: ${field}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find existing application by email
|
||||||
|
const existing = await pool.query(
|
||||||
|
'SELECT id, payment_status, submitted_at, status FROM applications WHERE email = $1',
|
||||||
|
[data.email.toLowerCase().trim()]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'No application found with this email' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = existing.rows[0];
|
||||||
|
|
||||||
|
// If already paid, don't allow re-submission
|
||||||
|
if (app.payment_status === 'paid') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'This application has already been paid. Contact us if you need to make changes.',
|
||||||
|
paid: true,
|
||||||
|
applicationId: app.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);
|
||||||
|
const selectedWeeks = Array.isArray(data.weeks) ? data.weeks : (data.weeks ? [data.weeks] : []);
|
||||||
|
const topThemes = Array.isArray(data.top_themes) ? data.top_themes : (data.top_themes ? [data.top_themes] : null);
|
||||||
|
|
||||||
|
// Update the application
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE applications SET
|
||||||
|
first_name = $1, last_name = $2, phone = $3, country = $4, city = $5,
|
||||||
|
pronouns = $6, date_of_birth = $7, occupation = $8, organization = $9,
|
||||||
|
skills = $10, languages = $11, website = $12, social_links = $13,
|
||||||
|
attendance_type = $14, arrival_date = $15, departure_date = $16,
|
||||||
|
accommodation_preference = $17, dietary_requirements = $18, dietary_notes = $19,
|
||||||
|
motivation = $20, contribution = $21, projects = $22, workshops_offer = $23,
|
||||||
|
commons_experience = $24, community_experience = $25, governance_interest = $26,
|
||||||
|
how_heard = $27, referral_name = $28, previous_events = $29,
|
||||||
|
emergency_name = $30, emergency_phone = $31, emergency_relationship = $32,
|
||||||
|
code_of_conduct_accepted = $33, privacy_policy_accepted = $34, photo_consent = $35,
|
||||||
|
scholarship_needed = $36, scholarship_reason = $37, need_accommodation = $38,
|
||||||
|
want_food = $39, accommodation_type = $40, selected_weeks = $41,
|
||||||
|
top_themes = $42, belief_update = $43, volunteer_interest = $44,
|
||||||
|
coupon_code = $45, food_preference = $46, accessibility_needs = $47
|
||||||
|
WHERE id = $48`,
|
||||||
|
[
|
||||||
|
data.first_name?.trim(),
|
||||||
|
data.last_name?.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.need_accommodation || false,
|
||||||
|
data.want_food || false,
|
||||||
|
data.accommodation_type || null,
|
||||||
|
selectedWeeks.length > 0 ? selectedWeeks : null,
|
||||||
|
topThemes,
|
||||||
|
data.belief_update?.trim() || null,
|
||||||
|
data.volunteer_interest || false,
|
||||||
|
data.coupon_code?.trim() || null,
|
||||||
|
data.food_preference?.trim() || null,
|
||||||
|
data.accessibility_needs?.trim() || null,
|
||||||
|
app.id
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const application = {
|
||||||
|
id: app.id,
|
||||||
|
submitted_at: app.submitted_at,
|
||||||
|
first_name: data.first_name,
|
||||||
|
last_name: data.last_name,
|
||||||
|
email: data.email,
|
||||||
|
weeks: selectedWeeks,
|
||||||
|
accommodation_type: data.accommodation_type || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create Mollie payment
|
||||||
|
let checkoutUrl = null;
|
||||||
|
if (selectedWeeks.length > 0 && process.env.MOLLIE_API_KEY) {
|
||||||
|
try {
|
||||||
|
const paymentResult = await createPayment(
|
||||||
|
app.id,
|
||||||
|
'registration',
|
||||||
|
selectedWeeks.length,
|
||||||
|
data.email.toLowerCase().trim(),
|
||||||
|
data.first_name,
|
||||||
|
data.last_name,
|
||||||
|
data.accommodation_type,
|
||||||
|
selectedWeeks
|
||||||
|
);
|
||||||
|
checkoutUrl = paymentResult.checkoutUrl;
|
||||||
|
console.log(`Mollie payment created (update): ${paymentResult.paymentId} (€${paymentResult.amount})`);
|
||||||
|
} catch (paymentError) {
|
||||||
|
console.error('Failed to create Mollie payment:', paymentError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Application updated successfully',
|
||||||
|
applicationId: app.id,
|
||||||
|
checkoutUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Application update error:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to update application. Please try again.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(405).json({ error: 'Method not allowed' });
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Lookup handler - public endpoint to check if an application exists by email
|
||||||
|
module.exports.lookup = async function lookupHandler(req, res) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== 'GET') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = (req.query.email || '').toLowerCase().trim();
|
||||||
|
if (!email || !email.includes('@')) {
|
||||||
|
return res.status(400).json({ error: 'Valid email is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT id, 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, need_accommodation, want_food,
|
||||||
|
accommodation_type, selected_weeks, top_themes, belief_update,
|
||||||
|
volunteer_interest, coupon_code, food_preference, accessibility_needs,
|
||||||
|
payment_status, submitted_at
|
||||||
|
FROM applications WHERE email = $1`,
|
||||||
|
[email]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ found: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = result.rows[0];
|
||||||
|
|
||||||
|
// Map DB column names to frontend form field names that restoreFormData() expects
|
||||||
|
const mapped = {
|
||||||
|
id: row.id,
|
||||||
|
first_name: row.first_name,
|
||||||
|
last_name: row.last_name,
|
||||||
|
email: row.email,
|
||||||
|
social_links: row.social_links,
|
||||||
|
how_heard: row.how_heard,
|
||||||
|
referral_name: row.referral_name,
|
||||||
|
affiliations: row.commons_experience,
|
||||||
|
motivation: row.motivation,
|
||||||
|
current_work: row.projects,
|
||||||
|
contribution: row.contribution,
|
||||||
|
themes_familiarity: row.workshops_offer,
|
||||||
|
belief_update: row.belief_update,
|
||||||
|
weeks: row.selected_weeks || [],
|
||||||
|
top_themes: row.top_themes || [],
|
||||||
|
need_accommodation: row.need_accommodation,
|
||||||
|
accommodation_type: row.accommodation_type,
|
||||||
|
food_preference: row.food_preference,
|
||||||
|
accessibility_needs: row.accessibility_needs,
|
||||||
|
volunteer_interest: row.volunteer_interest,
|
||||||
|
coupon_code: row.coupon_code,
|
||||||
|
privacy_policy_accepted: row.privacy_policy_accepted,
|
||||||
|
payment_status: row.payment_status,
|
||||||
|
submitted_at: row.submitted_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).json({ found: true, application: mapped });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Application lookup error:', error);
|
||||||
|
return res.status(500).json({ error: 'Lookup failed' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
134
apply.html
134
apply.html
|
|
@ -500,6 +500,48 @@
|
||||||
|
|
||||||
.next-steps-box li { margin-bottom: 0.5rem; }
|
.next-steps-box li { margin-bottom: 0.5rem; }
|
||||||
|
|
||||||
|
/* ===== Welcome-back modal ===== */
|
||||||
|
.modal-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
z-index: 200;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.modal-overlay.visible { display: flex; }
|
||||||
|
.modal-box {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.modal-box h3 {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
color: var(--forest);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.modal-box p { font-size: 0.95rem; color: #555; margin-bottom: 1.5rem; }
|
||||||
|
.modal-actions { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; }
|
||||||
|
.modal-actions .btn { min-width: 160px; }
|
||||||
|
.btn-secondary {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border: 2px solid var(--forest);
|
||||||
|
color: var(--forest);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { background: rgba(45, 80, 22, 0.05); }
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.container { padding: 1rem; }
|
.container { padding: 1rem; }
|
||||||
.form-section, .landing { padding: 1.5rem; }
|
.form-section, .landing { padding: 1.5rem; }
|
||||||
|
|
@ -551,6 +593,18 @@
|
||||||
<div class="progress-text">Step <span id="progress-step">1</span> of 10 — <span id="progress-percent">10</span>%</div>
|
<div class="progress-text">Step <span id="progress-step">1</span> of 10 — <span id="progress-percent">10</span>%</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Welcome-back modal -->
|
||||||
|
<div class="modal-overlay" id="welcome-back-modal">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 id="welcome-back-heading">Welcome back!</h3>
|
||||||
|
<p>We found your previous application. Would you like to load it and continue where you left off?</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="loadExistingApplication()">Load my application</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeWelcomeModal()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form id="application-form" style="display: none;">
|
<form id="application-form" style="display: none;">
|
||||||
<!-- Step 1: Which weeks + price calculator -->
|
<!-- Step 1: Which weeks + price calculator -->
|
||||||
<div class="form-section" data-step="1">
|
<div class="form-section" data-step="1">
|
||||||
|
|
@ -887,7 +941,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label style="display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer;">
|
<label style="display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer;">
|
||||||
<input type="checkbox" id="volunteer_interest" name="volunteer_interest" style="width: 18px; height: 18px; margin-top: 0.2rem; accent-color: var(--forest);">
|
<input type="checkbox" id="volunteer_interest" name="volunteer_interest" style="width: 18px; height: 18px; margin-top: 0.2rem; accent-color: var(--forest);">
|
||||||
<span>I'm interested in volunteering (e.g. helping with setup, cooking, facilitation) in exchange for a reduced fee</span>
|
<span>I'm interested in volunteering (e.g. helping with setup, cooking, facilitation)</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1111,17 +1165,24 @@
|
||||||
updateThemeCounter();
|
updateThemeCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Food preference
|
// Food preference — handle "allergies: <text>" from DB
|
||||||
if (data.food_preference) {
|
if (data.food_preference) {
|
||||||
const radio = document.querySelector(`input[name="food_preference"][value="${data.food_preference}"]`);
|
let foodVal = data.food_preference;
|
||||||
|
let allergyText = '';
|
||||||
|
if (foodVal.startsWith('allergies: ')) {
|
||||||
|
allergyText = foodVal.substring('allergies: '.length);
|
||||||
|
foodVal = 'allergies';
|
||||||
|
}
|
||||||
|
const radio = document.querySelector(`input[name="food_preference"][value="${foodVal}"]`);
|
||||||
if (radio) {
|
if (radio) {
|
||||||
radio.checked = true;
|
radio.checked = true;
|
||||||
if (data.food_preference === 'allergies') {
|
if (foodVal === 'allergies') {
|
||||||
document.getElementById('food-allergies-detail').style.display = 'block';
|
document.getElementById('food-allergies-detail').style.display = 'block';
|
||||||
|
if (allergyText) document.getElementById('food_allergies_text').value = allergyText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.food_allergies_text) {
|
if (data.food_allergies_text && !document.getElementById('food_allergies_text').value) {
|
||||||
document.getElementById('food_allergies_text').value = data.food_allergies_text;
|
document.getElementById('food_allergies_text').value = data.food_allergies_text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1223,8 +1284,42 @@
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextStep() {
|
// State for existing application resume flow
|
||||||
|
window._existingApplicationId = null;
|
||||||
|
window._pendingLookupData = null;
|
||||||
|
|
||||||
|
async function nextStep() {
|
||||||
if (!validateStep(currentStep)) return;
|
if (!validateStep(currentStep)) return;
|
||||||
|
|
||||||
|
// Email check when leaving step 2
|
||||||
|
if (currentStep === 2 && !window._existingApplicationId) {
|
||||||
|
const email = document.getElementById('email').value.trim().toLowerCase();
|
||||||
|
if (email) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/application/lookup?email=' + encodeURIComponent(email));
|
||||||
|
if (resp.ok) {
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.found) {
|
||||||
|
if (result.application.payment_status === 'paid') {
|
||||||
|
alert('This email already has a completed (paid) application. Contact us at contact@valleyofthecommons.com if you need to make changes.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Show welcome-back modal
|
||||||
|
window._pendingLookupData = result.application;
|
||||||
|
const heading = document.getElementById('welcome-back-heading');
|
||||||
|
heading.textContent = `Welcome back, ${result.application.first_name}!`;
|
||||||
|
document.getElementById('welcome-back-modal').classList.add('visible');
|
||||||
|
return; // Don't advance until user decides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 404 = no existing application, continue normally
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Email lookup failed:', e);
|
||||||
|
// Non-blocking — continue with new application
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentStep < totalSteps) {
|
if (currentStep < totalSteps) {
|
||||||
currentStep++;
|
currentStep++;
|
||||||
showStep(currentStep);
|
showStep(currentStep);
|
||||||
|
|
@ -1232,6 +1327,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadExistingApplication() {
|
||||||
|
const data = window._pendingLookupData;
|
||||||
|
if (!data) return;
|
||||||
|
window._existingApplicationId = data.id;
|
||||||
|
restoreFormData(data);
|
||||||
|
saveFormData();
|
||||||
|
document.getElementById('welcome-back-modal').classList.remove('visible');
|
||||||
|
// Advance to step 3
|
||||||
|
currentStep = 3;
|
||||||
|
showStep(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWelcomeModal() {
|
||||||
|
window._pendingLookupData = null;
|
||||||
|
document.getElementById('welcome-back-modal').classList.remove('visible');
|
||||||
|
// Stay on step 2 — user can change their email
|
||||||
|
}
|
||||||
|
|
||||||
function prevStep() {
|
function prevStep() {
|
||||||
if (currentStep > 1) {
|
if (currentStep > 1) {
|
||||||
currentStep--;
|
currentStep--;
|
||||||
|
|
@ -1629,13 +1742,14 @@
|
||||||
|
|
||||||
errorDiv.style.display = 'none';
|
errorDiv.style.display = 'none';
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.textContent = 'Submitting...';
|
const isUpdate = !!window._existingApplicationId;
|
||||||
|
submitBtn.textContent = isUpdate ? 'Updating...' : 'Submitting...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = collectFormData();
|
const data = collectFormData();
|
||||||
|
|
||||||
const response = await fetch('/api/application', {
|
const response = await fetch('/api/application', {
|
||||||
method: 'POST',
|
method: isUpdate ? 'PUT' : 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
|
@ -1649,7 +1763,9 @@
|
||||||
// Show success state
|
// Show success state
|
||||||
document.getElementById('confirm-name').textContent = data.first_name;
|
document.getElementById('confirm-name').textContent = data.first_name;
|
||||||
document.getElementById('confirm-email').textContent = data.email;
|
document.getElementById('confirm-email').textContent = data.email;
|
||||||
document.getElementById('success-heading').textContent = `You're in the process, ${data.first_name}!`;
|
document.getElementById('success-heading').textContent = isUpdate
|
||||||
|
? `Application updated, ${data.first_name}!`
|
||||||
|
: `You're in the process, ${data.first_name}!`;
|
||||||
|
|
||||||
document.querySelectorAll('.form-section').forEach(s => s.classList.remove('active'));
|
document.querySelectorAll('.form-section').forEach(s => s.classList.remove('active'));
|
||||||
document.querySelector('.form-section[data-step="success"]').style.display = 'block';
|
document.querySelector('.form-section[data-step="success"]').style.display = 'block';
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ app.use(express.urlencoded({ extended: true }));
|
||||||
// CORS middleware
|
// CORS middleware
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ app.use((req, res, next) => {
|
||||||
const waitlistHandler = require('./api/waitlist-db');
|
const waitlistHandler = require('./api/waitlist-db');
|
||||||
const newsletterHandler = require('./api/newsletter');
|
const newsletterHandler = require('./api/newsletter');
|
||||||
const applicationHandler = require('./api/application');
|
const applicationHandler = require('./api/application');
|
||||||
|
const applicationLookupHandler = require('./api/application').lookup;
|
||||||
const gameChatHandler = require('./api/game-chat');
|
const gameChatHandler = require('./api/game-chat');
|
||||||
const shareToGithubHandler = require('./api/share-to-github');
|
const shareToGithubHandler = require('./api/share-to-github');
|
||||||
const { handleWebhook, getPaymentStatus, resumePayment } = require('./api/mollie');
|
const { handleWebhook, getPaymentStatus, resumePayment } = require('./api/mollie');
|
||||||
|
|
@ -42,6 +43,7 @@ const vercelToExpress = (handler) => async (req, res) => {
|
||||||
app.all('/api/waitlist', vercelToExpress(waitlistHandler));
|
app.all('/api/waitlist', vercelToExpress(waitlistHandler));
|
||||||
app.all('/api/newsletter', vercelToExpress(newsletterHandler));
|
app.all('/api/newsletter', vercelToExpress(newsletterHandler));
|
||||||
app.all('/api/application', vercelToExpress(applicationHandler));
|
app.all('/api/application', vercelToExpress(applicationHandler));
|
||||||
|
app.get('/api/application/lookup', vercelToExpress(applicationLookupHandler));
|
||||||
app.all('/api/game-chat', vercelToExpress(gameChatHandler));
|
app.all('/api/game-chat', vercelToExpress(gameChatHandler));
|
||||||
app.all('/api/share-to-github', vercelToExpress(shareToGithubHandler));
|
app.all('/api/share-to-github', vercelToExpress(shareToGithubHandler));
|
||||||
app.post('/api/mollie/webhook', vercelToExpress(handleWebhook));
|
app.post('/api/mollie/webhook', vercelToExpress(handleWebhook));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue