worldplay-website/payment-success.html

356 lines
13 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>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">&#10003;</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 ~&euro;1520/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">&#10007;</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">&larr; 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">&euro;${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">&euro;${data.accommodationPrice.toFixed(2)}</span></div>`;
}
rows += `
<div class="row"><span>Processing fee (2%)</span><span class="val">&euro;${data.processingFee.toFixed(2)}</span></div>
<div class="row total"><span>Total paid</span><span class="val">&euro;${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} &mdash; 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>