356 lines
13 KiB
HTML
356 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Payment Confirmation — 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;
|
||
--text-primary: #f0f0f5;
|
||
--text-secondary: #a0a0b0;
|
||
--accent-purple: #9d4edd;
|
||
--accent-cyan: #00d9ff;
|
||
--accent-green: #00ff88;
|
||
--accent-orange: #ff9500;
|
||
--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: 600px;
|
||
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);
|
||
}
|
||
|
||
.status-icon {
|
||
text-align: center;
|
||
font-size: 4rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.status-title {
|
||
text-align: center;
|
||
font-size: 1.5rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.status-subtitle {
|
||
text-align: center;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 2rem;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.spinner {
|
||
display: inline-block;
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid var(--accent-purple);
|
||
border-top-color: transparent;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
vertical-align: middle;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
.details-table {
|
||
width: 100%;
|
||
margin-top: 1.5rem;
|
||
}
|
||
|
||
.details-table .row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 0.5rem 0;
|
||
font-size: 0.9rem;
|
||
color: var(--text-secondary);
|
||
border-bottom: 1px solid rgba(157, 78, 221, 0.1);
|
||
}
|
||
|
||
.details-table .row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.details-table .row.total {
|
||
border-top: 1px solid rgba(157, 78, 221, 0.3);
|
||
border-bottom: none;
|
||
margin-top: 0.5rem;
|
||
padding-top: 0.75rem;
|
||
font-weight: 700;
|
||
font-size: 1.05rem;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.details-table .row.total .val {
|
||
color: var(--accent-green);
|
||
}
|
||
|
||
.details-table .val {
|
||
font-family: 'Space Mono', monospace;
|
||
}
|
||
|
||
.booking-info {
|
||
margin-top: 1.5rem;
|
||
padding: 1rem 1.25rem;
|
||
background: rgba(0, 217, 255, 0.06);
|
||
border: 1px solid rgba(0, 217, 255, 0.2);
|
||
border-radius: 10px;
|
||
font-size: 0.9rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.booking-info strong {
|
||
color: var(--accent-cyan);
|
||
}
|
||
|
||
.food-note {
|
||
margin-top: 1rem;
|
||
padding: 0.75rem 1rem;
|
||
background: rgba(255, 149, 0, 0.08);
|
||
border: 1px solid rgba(255, 149, 0, 0.2);
|
||
border-radius: 10px;
|
||
font-size: 0.8rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.food-note 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; }
|
||
|
||
.status-paid .status-icon::after { content: ''; }
|
||
.status-pending .status-icon::after { content: ''; }
|
||
.status-failed .status-icon::after { content: ''; }
|
||
|
||
#content-paid, #content-failed { display: none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="logo">
|
||
<h1>WORLDPLAY</h1>
|
||
<p>To be Defined</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<!-- Pending state (polling) -->
|
||
<div id="content-pending">
|
||
<div class="status-icon">
|
||
<span class="spinner" style="width:48px;height:48px;border-width:4px;"></span>
|
||
</div>
|
||
<h2 class="status-title">Confirming Payment...</h2>
|
||
<p class="status-subtitle">Waiting for payment confirmation from Mollie. This usually takes a few seconds.</p>
|
||
</div>
|
||
|
||
<!-- Paid state -->
|
||
<div id="content-paid">
|
||
<div class="status-icon">✓</div>
|
||
<h2 class="status-title" style="color: var(--accent-green);">Payment Confirmed!</h2>
|
||
<p class="status-subtitle">You're in, <strong id="guest-name"></strong>! A confirmation email is on its way.</p>
|
||
|
||
<div class="details-table" id="payment-details"></div>
|
||
|
||
<div id="booking-section"></div>
|
||
|
||
<div class="food-note">
|
||
<strong>About food:</strong> Food is not included. Expect ~€15–20/person/day. We'll be in touch about food choices before the event.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Failed/expired state -->
|
||
<div id="content-failed">
|
||
<div class="status-icon">✗</div>
|
||
<h2 class="status-title" style="color: #ff006e;">Payment Not Completed</h2>
|
||
<p class="status-subtitle" id="failed-message">Your payment was not completed. You can return to the registration page to try again.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<a href="/" class="back-link">← Back to worldplay.art</a>
|
||
</div>
|
||
|
||
<script>
|
||
const params = new URLSearchParams(window.location.search);
|
||
const regId = params.get('id');
|
||
|
||
const contentPending = document.getElementById('content-pending');
|
||
const contentPaid = document.getElementById('content-paid');
|
||
const contentFailed = document.getElementById('content-failed');
|
||
|
||
if (!regId) {
|
||
contentPending.style.display = 'none';
|
||
contentFailed.style.display = 'block';
|
||
document.getElementById('failed-message').textContent = 'No registration ID found. Please return to the registration page.';
|
||
} else {
|
||
let attempts = 0;
|
||
const maxAttempts = 60; // 2 minutes at 2s intervals
|
||
|
||
async function pollStatus() {
|
||
try {
|
||
const res = await fetch(`/api/payment-status?id=${encodeURIComponent(regId)}`);
|
||
if (!res.ok) throw new Error('Failed to fetch status');
|
||
const data = await res.json();
|
||
|
||
if (data.paymentStatus === 'paid') {
|
||
showPaid(data);
|
||
return;
|
||
}
|
||
|
||
if (data.paymentStatus === 'failed' || data.paymentStatus === 'canceled' || data.paymentStatus === 'expired') {
|
||
showFailed(data.paymentStatus);
|
||
return;
|
||
}
|
||
|
||
// Still pending
|
||
attempts++;
|
||
if (attempts < maxAttempts) {
|
||
setTimeout(pollStatus, 2000);
|
||
} else {
|
||
showFailed('timeout');
|
||
}
|
||
} catch (err) {
|
||
attempts++;
|
||
if (attempts < maxAttempts) {
|
||
setTimeout(pollStatus, 3000);
|
||
} else {
|
||
showFailed('error');
|
||
}
|
||
}
|
||
}
|
||
|
||
function showPaid(data) {
|
||
contentPending.style.display = 'none';
|
||
contentPaid.style.display = 'block';
|
||
|
||
document.getElementById('guest-name').textContent = `${data.firstName} ${data.lastName}`;
|
||
|
||
// Days label
|
||
const DAY_LABELS = {
|
||
'2026-06-07': 'Jun 7', '2026-06-08': 'Jun 8', '2026-06-09': 'Jun 9',
|
||
'2026-06-10': 'Jun 10', '2026-06-11': 'Jun 11', '2026-06-12': 'Jun 12',
|
||
'2026-06-13': 'Jun 13',
|
||
};
|
||
let daysText = 'Full week';
|
||
if (!data.isFullWeek && Array.isArray(data.selectedDays)) {
|
||
daysText = data.numDays + ' day' + (data.numDays > 1 ? 's' : '') + ': ' +
|
||
data.selectedDays.map(d => DAY_LABELS[d] || d).join(', ');
|
||
}
|
||
|
||
// Participation label
|
||
const participationLabel = data.isFullWeek
|
||
? 'Participation (full week)'
|
||
: `Participation (${data.numDays} day${data.numDays > 1 ? 's' : ''})`;
|
||
|
||
let rows = `
|
||
<div class="row"><span style="color:var(--accent-cyan);font-size:0.85rem;">${daysText}</span><span></span></div>
|
||
<div class="row"><span>${participationLabel}</span><span class="val">€${data.participationFee.toFixed(2)}</span></div>
|
||
`;
|
||
if (data.accommodationPrice > 0) {
|
||
let accomNightsLabel;
|
||
if (data.isFullWeek) {
|
||
accomNightsLabel = `${data.accommodationLabel} (7 nights)`;
|
||
} else {
|
||
const nightlyRate = Math.round(data.accommodationPrice / data.numDays);
|
||
accomNightsLabel = `${data.accommodationLabel} (${data.numDays} night${data.numDays > 1 ? 's' : ''} \u00D7 \u20AC${nightlyRate})`;
|
||
}
|
||
rows += `<div class="row"><span>${accomNightsLabel}</span><span class="val">€${data.accommodationPrice.toFixed(2)}</span></div>`;
|
||
}
|
||
rows += `
|
||
<div class="row"><span>Processing fee (2%)</span><span class="val">€${data.processingFee.toFixed(2)}</span></div>
|
||
<div class="row total"><span>Total paid</span><span class="val">€${data.total.toFixed(2)}</span></div>
|
||
`;
|
||
document.getElementById('payment-details').innerHTML = rows;
|
||
|
||
if (data.bookingResult && data.bookingResult.success) {
|
||
document.getElementById('booking-section').innerHTML = `
|
||
<div class="booking-info">
|
||
<strong>Your accommodation:</strong> ${data.bookingResult.venue} — Room ${data.bookingResult.room} (${data.bookingResult.bedType})
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
function showFailed(reason) {
|
||
contentPending.style.display = 'none';
|
||
contentFailed.style.display = 'block';
|
||
|
||
const messages = {
|
||
failed: 'Your payment was declined. Please try again or use a different payment method.',
|
||
canceled: 'You cancelled the payment. You can return to the registration page to try again.',
|
||
expired: 'Your payment session expired. Please return to the registration page to try again.',
|
||
timeout: 'We couldn\'t confirm your payment yet. If you completed payment, please check your email — confirmation may arrive shortly.',
|
||
error: 'Something went wrong checking your payment status. Please check your email for confirmation.',
|
||
};
|
||
document.getElementById('failed-message').textContent = messages[reason] || messages.error;
|
||
}
|
||
|
||
pollStatus();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|