worldplay-website/pay.html

825 lines
31 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Complete Payment — WORLDPLAY</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌍</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #0a0a0f;
--bg-card: #12121a;
--bg-input: #1a1a2e;
--text-primary: #f0f0f5;
--text-secondary: #a0a0b0;
--accent-purple: #9d4edd;
--accent-cyan: #00d9ff;
--accent-green: #00ff88;
--accent-orange: #ff9500;
--accent-pink: #ff006e;
--gradient-1: linear-gradient(135deg, #9d4edd 0%, #ff006e 100%);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Space Grotesk', sans-serif;
background-color: var(--bg-dark);
color: var(--text-primary);
line-height: 1.7;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.container {
max-width: 620px;
width: 100%;
}
.logo {
text-align: center;
margin-bottom: 2rem;
}
.logo h1 {
font-family: 'Space Mono', monospace;
font-size: 2rem;
background: var(--gradient-1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo p {
color: var(--text-secondary);
font-style: italic;
margin-top: 4px;
}
.card {
background: var(--bg-card);
border: 1px solid rgba(157, 78, 221, 0.3);
border-radius: 16px;
padding: 2.5rem;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
background: var(--gradient-1);
}
.card h2 {
font-size: 1.4rem;
margin-bottom: 0.5rem;
}
.card > p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
font-size: 0.95rem;
}
label {
display: block;
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
input[type="email"] {
width: 100%;
padding: 0.85rem 1rem;
background: var(--bg-input);
border: 2px solid rgba(157, 78, 221, 0.2);
border-radius: 10px;
color: var(--text-primary);
font-family: 'Space Grotesk', sans-serif;
font-size: 1rem;
outline: none;
transition: border-color 0.3s;
}
input[type="email"]:focus {
border-color: var(--accent-purple);
}
.btn {
display: inline-block;
padding: 0.9rem 2rem;
border: none;
border-radius: 50px;
font-family: 'Space Grotesk', sans-serif;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
text-align: center;
}
.btn-primary {
background: var(--gradient-1);
color: white;
width: 100%;
margin-top: 1rem;
}
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.form-message {
margin-top: 1rem;
padding: 0.75rem 1rem;
border-radius: 8px;
font-size: 0.9rem;
display: none;
}
.form-message.error {
display: block;
background: rgba(255, 0, 110, 0.1);
border: 1px solid rgba(255, 0, 110, 0.3);
color: var(--accent-pink);
}
.form-message.success {
display: block;
background: rgba(0, 255, 136, 0.08);
border: 1px solid rgba(0, 255, 136, 0.3);
color: var(--accent-green);
}
.form-message a {
color: var(--accent-cyan);
}
/* Accommodation UI (mirrors index.html) */
.accom-toggle {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.accom-toggle-btn {
flex: 1;
padding: 1rem;
background: var(--bg-input);
border: 2px solid rgba(157, 78, 221, 0.2);
border-radius: 10px;
color: var(--text-secondary);
font-family: 'Space Grotesk', sans-serif;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.accom-toggle-btn:hover { border-color: var(--accent-purple); }
.accom-toggle-btn.selected {
border-color: var(--accent-cyan);
background: rgba(0, 217, 255, 0.08);
color: var(--text-primary);
}
.accom-cards {
display: none;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.accom-cards.visible { display: flex; }
.accom-venue-label {
font-size: 0.8rem;
color: var(--accent-purple);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-top: 0.75rem;
margin-bottom: 0.25rem;
}
.accom-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
background: var(--bg-input);
border: 2px solid rgba(157, 78, 221, 0.15);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.accom-card:hover {
border-color: var(--accent-purple);
background: rgba(157, 78, 221, 0.08);
}
.accom-card.selected {
border-color: var(--accent-cyan);
background: rgba(0, 217, 255, 0.08);
}
.accom-card .accom-name {
font-size: 0.95rem;
color: var(--text-primary);
}
.accom-card .accom-price {
font-family: 'Space Mono', monospace;
font-size: 0.9rem;
color: var(--accent-cyan);
white-space: nowrap;
}
.accom-card .accom-note {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 2px;
}
.price-summary {
background: rgba(157, 78, 221, 0.08);
border: 1px solid rgba(157, 78, 221, 0.3);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.price-summary h4 {
font-size: 0.85rem;
color: var(--accent-purple);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 1rem;
}
.price-line {
display: flex;
justify-content: space-between;
padding: 0.4rem 0;
font-size: 0.9rem;
color: var(--text-secondary);
}
.price-line.total {
border-top: 1px solid rgba(157, 78, 221, 0.3);
margin-top: 0.5rem;
padding-top: 0.75rem;
font-weight: 700;
font-size: 1.1rem;
color: var(--text-primary);
}
.price-line.total .price-amount { color: var(--accent-green); }
.price-amount { font-family: 'Space Mono', monospace; }
.info-box {
background: rgba(255, 149, 0, 0.08);
border: 1px solid rgba(255, 149, 0, 0.3);
border-radius: 10px;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.6;
}
.info-box strong { color: var(--accent-orange); }
.back-link {
display: block;
text-align: center;
margin-top: 2rem;
color: var(--accent-cyan);
text-decoration: none;
font-size: 0.9rem;
}
.back-link:hover { text-decoration: underline; }
.welcome-name {
color: var(--accent-cyan);
}
#state-days { display: none; }
#state-payment { display: none; }
.day-selection-header {
text-align: center;
margin-bottom: 1.5rem;
}
.fullweek-btn {
display: block;
width: 100%;
padding: 1.1rem;
background: rgba(0, 217, 255, 0.08);
border: 2px solid rgba(0, 217, 255, 0.3);
border-radius: 12px;
color: var(--accent-cyan);
font-family: 'Space Grotesk', sans-serif;
font-size: 1.05rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 1rem;
}
.fullweek-btn:hover { border-color: var(--accent-cyan); background: rgba(0, 217, 255, 0.15); }
.fullweek-btn.selected { border-color: var(--accent-green); background: rgba(0, 255, 136, 0.1); color: var(--accent-green); }
.day-or { text-align: center; color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 1rem; }
.day-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.day-btn {
padding: 0.7rem 0.4rem;
background: var(--bg-input);
border: 2px solid rgba(157, 78, 221, 0.2);
border-radius: 10px;
color: var(--text-secondary);
font-family: 'Space Grotesk', sans-serif;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.day-btn:hover { border-color: var(--accent-purple); }
.day-btn.selected { border-color: var(--accent-cyan); background: rgba(0, 217, 255, 0.08); color: var(--text-primary); }
.day-price-note {
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 1rem;
text-align: center;
}
.day-price-note strong { color: var(--accent-cyan); font-family: 'Space Mono', monospace; }
#hh-section { display: block; }
#hh-section.hidden { display: none; }
</style>
</head>
<body>
<div class="container">
<div class="logo">
<h1>WORLDPLAY</h1>
<p>To be Defined</p>
</div>
<div class="card">
<!-- State 1: Email lookup -->
<div id="state-lookup">
<h2>Complete Your Payment</h2>
<p>Already registered? Enter your email to pick up where you left off.</p>
<label for="email">Email address</label>
<input type="email" id="email" placeholder="you@example.com" autocomplete="email">
<button class="btn btn-primary" id="btn-lookup">Look up my registration</button>
<div class="form-message" id="lookup-msg"></div>
</div>
<!-- State 2: Day Selection -->
<div id="state-days">
<h2>Welcome back, <span class="welcome-name" id="welcome-name-days"></span>!</h2>
<p>Which days will you attend?</p>
<button type="button" class="fullweek-btn" id="btn-fullweek">Full Week — June 713 (&euro;50)</button>
<div class="day-or">— or pick individual days (&euro;10/day) —</div>
<div class="day-grid">
<button type="button" class="day-btn" data-day="2026-06-07">Jun 7<br><small>Sat</small></button>
<button type="button" class="day-btn" data-day="2026-06-08">Jun 8<br><small>Sun</small></button>
<button type="button" class="day-btn" data-day="2026-06-09">Jun 9<br><small>Mon</small></button>
<button type="button" class="day-btn" data-day="2026-06-10">Jun 10<br><small>Tue</small></button>
<button type="button" class="day-btn" data-day="2026-06-11">Jun 11<br><small>Wed</small></button>
<button type="button" class="day-btn" data-day="2026-06-12">Jun 12<br><small>Thu</small></button>
<button type="button" class="day-btn" data-day="2026-06-13">Jun 13<br><small>Fri</small></button>
</div>
<div class="day-price-note" id="day-price-note" style="display:none;">
<span id="day-count">0</span> day(s) selected — <strong>&euro;<span id="day-price">0</span></strong> participation
</div>
<button class="btn btn-primary" id="btn-continue-days" disabled>Continue to Accommodation</button>
<div class="form-message" id="days-msg"></div>
</div>
<!-- State 3: Accommodation & Payment -->
<div id="state-payment">
<h2>Welcome back, <span class="welcome-name" id="welcome-name"></span>!</h2>
<p>Choose your accommodation and complete payment.</p>
<div class="form-group">
<label>Do you need on-site accommodation? (7 nights, June 7-13)</label>
<div class="accom-toggle">
<button type="button" class="accom-toggle-btn selected" data-accom="no">No, I'll arrange my own</button>
<button type="button" class="accom-toggle-btn" data-accom="yes">Yes, book a room</button>
</div>
</div>
<div class="accom-cards" id="accom-cards">
<div class="accom-venue-label">Commons Hub</div>
<div class="accom-card" data-type="ch-shared" data-price="275" data-nightly-rate="40">
<div>
<div class="accom-name">Shared Room</div>
<div class="accom-note">Bunk beds / shared room</div>
</div>
<div class="accom-price">&euro;275</div>
</div>
<div class="accom-card" data-type="ch-double" data-price="350" data-nightly-rate="50">
<div>
<div class="accom-name">Double Room</div>
<div class="accom-note">Double bed, private or shared</div>
</div>
<div class="accom-price">&euro;350</div>
</div>
<div id="hh-section">
<div class="accom-venue-label">Herrnhof Villa</div>
<div class="accom-card" data-type="hh-living" data-price="315">
<div>
<div class="accom-name">Living Room</div>
<div class="accom-note">Sofa bed / daybed in shared living area</div>
</div>
<div class="accom-price">&euro;315</div>
</div>
<div class="accom-card" data-type="hh-triple" data-price="350">
<div>
<div class="accom-name">Triple Room</div>
<div class="accom-note">Shared with two others</div>
</div>
<div class="accom-price">&euro;350</div>
</div>
<div class="accom-card" data-type="hh-twin" data-price="420">
<div>
<div class="accom-name">Twin Room</div>
<div class="accom-note">Two separate beds, shared with one other</div>
</div>
<div class="accom-price">&euro;420</div>
</div>
<div class="accom-card" data-type="hh-single" data-price="665">
<div>
<div class="accom-name">Single Room</div>
<div class="accom-note">Private room for one</div>
</div>
<div class="accom-price">&euro;665</div>
</div>
<div class="accom-card" data-type="hh-couple" data-price="700">
<div>
<div class="accom-name">Couple Room</div>
<div class="accom-note">Double bed, private room for two</div>
</div>
<div class="accom-price">&euro;700</div>
</div>
</div>
</div>
<div class="price-summary" id="price-summary">
<h4>Price Summary</h4>
<div class="price-line">
<span id="price-participation-label">Participation fee</span>
<span class="price-amount" id="price-participation-amount">&euro;50.00</span>
</div>
<div class="price-line" id="price-accom-line" style="display: none;">
<span id="price-accom-label">Accommodation</span>
<span class="price-amount" id="price-accom-amount">&euro;0.00</span>
</div>
<div class="price-line">
<span>Processing fee (2%)</span>
<span class="price-amount" id="price-processing">&euro;1.00</span>
</div>
<div class="price-line total">
<span>Total</span>
<span class="price-amount" id="price-total">&euro;51.00</span>
</div>
</div>
<div class="info-box">
<strong>About food:</strong> Food is not included in this payment. Expect approximately &euro;15-20 per person per day. We'll be in touch about food choices and dietary preferences before the event.
</div>
<div class="info-box" style="background: rgba(157, 78, 221, 0.08); border-color: rgba(157, 78, 221, 0.3);">
<strong style="color: var(--accent-purple);">Deposit &amp; cancellation:</strong> This is a full payment. If you cancel more than 1 month before the event, you'll receive a 70% refund. If you cancel between 1 month and 1 week before, you'll receive a 30% refund. No refunds within 1 week of the event.
</div>
<button class="btn btn-primary" id="btn-pay">Pay via Mollie</button>
<div class="form-message" id="payment-msg"></div>
</div>
</div>
<a href="/" class="back-link">&larr; Back to worldplay.art</a>
</div>
<script>
const stateLookup = document.getElementById('state-lookup');
const stateDays = document.getElementById('state-days');
const statePayment = document.getElementById('state-payment');
const emailInput = document.getElementById('email');
const btnLookup = document.getElementById('btn-lookup');
const lookupMsg = document.getElementById('lookup-msg');
const paymentMsg = document.getElementById('payment-msg');
let registrationId = null;
let selectedAccom = null;
let selectedDays = null; // 'full-week' or array of date strings
let isFullWeek = false;
function showMsg(el, html, type) {
el.innerHTML = html;
el.className = 'form-message ' + type;
}
function clearMsg(el) {
el.innerHTML = '';
el.className = 'form-message';
}
// Allow Enter key to submit
emailInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') btnLookup.click();
});
// Email lookup
btnLookup.addEventListener('click', async () => {
clearMsg(lookupMsg);
const email = emailInput.value.trim();
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showMsg(lookupMsg, 'Please enter a valid email address.', 'error');
return;
}
btnLookup.disabled = true;
btnLookup.textContent = 'Looking up...';
try {
const res = await fetch(`/api/lookup-registration?email=${encodeURIComponent(email)}`);
const data = await res.json();
if (res.status === 404) {
showMsg(lookupMsg, 'No registration found for this email. <a href="/#register">Register here</a>.', 'error');
return;
}
if (!res.ok) {
throw new Error(data.error || 'Lookup failed');
}
if (data.paymentStatus === 'paid') {
showMsg(lookupMsg, 'Your payment is already confirmed! Check your email for your booking details.', 'success');
return;
}
// Show day-selection state
registrationId = data.id;
document.getElementById('welcome-name-days').textContent = data.firstName;
document.getElementById('welcome-name').textContent = data.firstName;
stateLookup.style.display = 'none';
stateDays.style.display = 'block';
} catch (err) {
showMsg(lookupMsg, err.message, 'error');
} finally {
btnLookup.disabled = false;
btnLookup.textContent = 'Look up my registration';
}
});
// ===== Day selection logic =====
const btnFullWeek = document.getElementById('btn-fullweek');
const dayBtns = document.querySelectorAll('.day-btn');
const dayPriceNote = document.getElementById('day-price-note');
const dayCountEl = document.getElementById('day-count');
const dayPriceEl = document.getElementById('day-price');
const btnContinueDays = document.getElementById('btn-continue-days');
function getSelectedIndividualDays() {
return Array.from(dayBtns).filter(b => b.classList.contains('selected')).map(b => b.dataset.day);
}
function updateDaySelection() {
const days = getSelectedIndividualDays();
if (days.length === 7) {
// Auto-promote to full-week
setFullWeek(true);
return;
}
if (days.length > 0) {
isFullWeek = false;
selectedDays = days;
dayPriceNote.style.display = 'block';
dayCountEl.textContent = days.length;
dayPriceEl.textContent = (days.length * 10);
btnFullWeek.classList.remove('selected');
btnContinueDays.disabled = false;
} else if (!isFullWeek) {
selectedDays = null;
dayPriceNote.style.display = 'none';
btnContinueDays.disabled = true;
}
}
function setFullWeek(on) {
isFullWeek = on;
if (on) {
selectedDays = 'full-week';
btnFullWeek.classList.add('selected');
dayBtns.forEach(b => b.classList.remove('selected'));
dayPriceNote.style.display = 'none';
btnContinueDays.disabled = false;
} else {
selectedDays = null;
btnFullWeek.classList.remove('selected');
btnContinueDays.disabled = true;
}
}
btnFullWeek.addEventListener('click', () => {
setFullWeek(!isFullWeek);
if (!isFullWeek) updateDaySelection();
});
dayBtns.forEach(btn => {
btn.addEventListener('click', () => {
btn.classList.toggle('selected');
if (isFullWeek) {
isFullWeek = false;
btnFullWeek.classList.remove('selected');
}
updateDaySelection();
});
});
// Continue to accommodation
btnContinueDays.addEventListener('click', () => {
stateDays.style.display = 'none';
statePayment.style.display = 'block';
// Show/hide Villa section based on full-week
const hhSection = document.getElementById('hh-section');
if (isFullWeek) {
hhSection.classList.remove('hidden');
} else {
hhSection.classList.add('hidden');
// Deselect any villa selection
if (selectedAccom && selectedAccom.startsWith('hh-')) {
selectedAccom = null;
document.querySelectorAll('.accom-card').forEach(c => c.classList.remove('selected'));
}
}
// Update accommodation card prices for partial-week
document.querySelectorAll('.accom-card[data-nightly-rate]').forEach(card => {
const nightlyRate = parseInt(card.dataset.nightlyRate);
const numDays = isFullWeek ? 7 : selectedDays.length;
if (isFullWeek) {
// Show weekly price
card.querySelector('.accom-price').textContent = '\u20AC' + parseInt(card.dataset.price);
} else {
// Show per-night price
const total = nightlyRate * numDays;
card.querySelector('.accom-price').textContent = '\u20AC' + total + ' (' + numDays + ' × \u20AC' + nightlyRate + ')';
}
});
updatePriceSummary();
});
// ===== Accommodation toggle =====
const accomCards = document.getElementById('accom-cards');
document.querySelectorAll('.accom-toggle-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.accom-toggle-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
if (btn.dataset.accom === 'yes') {
accomCards.classList.add('visible');
} else {
accomCards.classList.remove('visible');
selectedAccom = null;
document.querySelectorAll('.accom-card').forEach(c => c.classList.remove('selected'));
updatePriceSummary();
}
});
});
// Accommodation card selection
document.querySelectorAll('.accom-card').forEach(card => {
card.addEventListener('click', () => {
document.querySelectorAll('.accom-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedAccom = card.dataset.type;
updatePriceSummary();
});
});
// Live price calculator
function updatePriceSummary() {
const numDays = isFullWeek ? 7 : (Array.isArray(selectedDays) ? selectedDays.length : 7);
const participation = isFullWeek ? 50 : numDays * 10;
const participationLabel = document.getElementById('price-participation-label');
const participationAmount = document.getElementById('price-participation-amount');
const accomLine = document.getElementById('price-accom-line');
const accomLabel = document.getElementById('price-accom-label');
const accomAmount = document.getElementById('price-accom-amount');
const processingEl = document.getElementById('price-processing');
const totalEl = document.getElementById('price-total');
participationLabel.textContent = isFullWeek
? 'Participation fee (full week)'
: 'Participation fee (' + numDays + ' day' + (numDays > 1 ? 's' : '') + ')';
participationAmount.textContent = '\u20AC' + participation.toFixed(2);
let accomPrice = 0;
if (selectedAccom) {
const card = document.querySelector(`.accom-card[data-type="${selectedAccom}"]`);
if (isFullWeek) {
accomPrice = parseInt(card.dataset.price);
accomLabel.textContent = card.querySelector('.accom-name').textContent + ' (7 nights)';
} else {
const nightlyRate = parseInt(card.dataset.nightlyRate);
accomPrice = nightlyRate * numDays;
accomLabel.textContent = card.querySelector('.accom-name').textContent + ' (' + numDays + ' night' + (numDays > 1 ? 's' : '') + ' \u00D7 \u20AC' + nightlyRate + ')';
}
accomAmount.textContent = '\u20AC' + accomPrice.toFixed(2);
accomLine.style.display = 'flex';
} else {
accomLine.style.display = 'none';
}
const subtotal = participation + accomPrice;
const processing = Math.round(subtotal * 0.02 * 100) / 100;
const total = subtotal + processing;
processingEl.textContent = '\u20AC' + processing.toFixed(2);
totalEl.textContent = '\u20AC' + total.toFixed(2);
}
// Pay button
const btnPay = document.getElementById('btn-pay');
btnPay.addEventListener('click', async () => {
clearMsg(paymentMsg);
const accomToggled = document.querySelector('.accom-toggle-btn[data-accom="yes"]').classList.contains('selected');
if (accomToggled && !selectedAccom) {
showMsg(paymentMsg, 'Please select an accommodation option.', 'error');
return;
}
btnPay.disabled = true;
btnPay.textContent = 'Creating payment...';
try {
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
registrationId,
accommodationType: selectedAccom || null,
selectedDays,
}),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Failed to create payment');
}
window.location.href = result.checkoutUrl;
} catch (error) {
showMsg(paymentMsg, error.message, 'error');
btnPay.disabled = false;
btnPay.textContent = 'Pay via Mollie';
}
});
</script>
</body>
</html>