source: rsvp.html@ d8deee6

main
Last change on this file since d8deee6 was d8deee6, checked in by GitHub <noreply@โ€ฆ>, 3 months ago

Add files via upload

  • Property mode set to 100644
File size: 17.5 KB
Lineย 
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8"/>
5 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6 <title>Wedding RSVP</title>
7 <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,600&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"/>
8 <style>
9 :root {
10 --cream:#fdf8f3; --cream-2:#f9f1e8;
11 --blush-light:#fce8e8; --blush-mid:#e8949c;
12 --rose:#d4606a; --rose-deep:#b84550;
13 --gold:#c9a84c; --gold-dark:#9e7a28;
14 --text-dark:#2c1f1f; --text-mid:#6b4f4f; --text-light:#a08080;
15 --border:#ecddd0; --border-light:#f4ebe2;
16 --green:#3a9e6b; --green-light:#e4f5ec;
17 --red:#c94545; --red-light:#fdeaea;
18 --shadow-lg:0 16px 40px rgba(44,31,31,.14),0 4px 12px rgba(44,31,31,.08);
19 --radius-md:10px; --radius-lg:16px; --radius-xl:22px;
20 --transition:200ms cubic-bezier(.4,0,.2,1);
21 }
22 *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
23 html,body{
24 height:100%; font-family:'DM Sans',sans-serif;
25 background:linear-gradient(135deg,#fdf8f3,#f9f1e8,#fce8e8);
26 color:var(--text-dark); -webkit-font-smoothing:antialiased;
27 display:flex; align-items:center; justify-content:center;
28 min-height:100vh; padding:20px;
29 }
30
31 .rsvp-wrapper { width:100%; max-width:520px; }
32
33 /* logo */
34 .rsvp-logo {
35 text-align:center; margin-bottom:24px;
36 }
37 .rsvp-logo .mark {
38 width:48px; height:48px; margin:0 auto 10px;
39 background:linear-gradient(135deg,var(--rose),var(--blush-mid));
40 border-radius:12px;
41 display:flex; align-items:center; justify-content:center;
42 box-shadow:0 4px 16px rgba(212,96,106,.35);
43 }
44 .rsvp-logo .mark svg { width:24px; height:24px; }
45 .rsvp-logo h1 {
46 font-family:'Cormorant Garamond',serif;
47 font-size:24px; font-weight:700;
48 }
49
50 /* card */
51 .rsvp-card {
52 background:#fff; border-radius:var(--radius-xl);
53 border:1px solid var(--border-light);
54 box-shadow:var(--shadow-lg); overflow:hidden;
55 }
56
57 /* header band */
58 .rsvp-header {
59 background:linear-gradient(135deg,var(--rose),var(--blush-mid));
60 padding:28px 32px; text-align:center; color:#fff;
61 }
62 .rsvp-header .emoji { font-size:40px; margin-bottom:8px; }
63 .rsvp-header h2 {
64 font-family:'Cormorant Garamond',serif;
65 font-size:26px; font-weight:700; margin-bottom:4px;
66 }
67 .rsvp-header p { font-size:14px; opacity:.9; }
68
69 /* body */
70 .rsvp-body { padding:32px; }
71
72 .gold-rule {
73 width:36px; height:2px;
74 background:linear-gradient(90deg,var(--gold),transparent);
75 border-radius:2px; margin:0 0 20px;
76 }
77
78 /* event info box */
79 .event-box {
80 background:var(--cream); border:1px solid var(--border-light);
81 border-radius:var(--radius-lg); padding:18px 20px;
82 margin-bottom:24px;
83 }
84 .event-box .ev-label {
85 font-size:10px; font-weight:700; text-transform:uppercase;
86 letter-spacing:.1em; color:var(--text-light); margin-bottom:6px;
87 }
88 .event-box .ev-type {
89 font-family:'Cormorant Garamond',serif;
90 font-size:22px; font-weight:700; color:var(--text-dark);
91 margin-bottom:6px;
92 }
93 .event-box .ev-meta {
94 font-size:13px; color:var(--text-mid);
95 display:flex; gap:16px; flex-wrap:wrap;
96 }
97 .event-box .ev-meta span { display:flex; align-items:center; gap:5px; }
98
99 /* guest greeting */
100 .guest-greeting {
101 font-size:15px; color:var(--text-mid);
102 margin-bottom:24px; line-height:1.6;
103 }
104 .guest-greeting strong { color:var(--text-dark); }
105
106 /* RSVP buttons */
107 .rsvp-actions {
108 display:grid; grid-template-columns:1fr 1fr; gap:14px;
109 margin-bottom:20px;
110 }
111 .rsvp-btn {
112 padding:16px 12px; border-radius:var(--radius-lg);
113 border:2px solid transparent; cursor:pointer;
114 font-family:'DM Sans',sans-serif; font-size:15px; font-weight:600;
115 display:flex; flex-direction:column; align-items:center; gap:6px;
116 transition:all var(--transition);
117 }
118 .rsvp-btn .btn-icon { font-size:28px; }
119 .rsvp-btn-accept {
120 background:var(--green-light); color:var(--green);
121 border-color:#b2e0ca;
122 }
123 .rsvp-btn-accept:hover {
124 background:var(--green); color:#fff; border-color:var(--green);
125 transform:translateY(-2px); box-shadow:0 6px 20px rgba(58,158,107,.3);
126 }
127 .rsvp-btn-decline {
128 background:var(--red-light); color:var(--red);
129 border-color:#f5c2c2;
130 }
131 .rsvp-btn-decline:hover {
132 background:var(--red); color:#fff; border-color:var(--red);
133 transform:translateY(-2px); box-shadow:0 6px 20px rgba(201,69,69,.3);
134 }
135 .rsvp-btn:disabled {
136 opacity:.5; cursor:not-allowed; transform:none !important;
137 }
138 .rsvp-btn.selected-accept {
139 background:var(--green); color:#fff; border-color:var(--green);
140 }
141 .rsvp-btn.selected-decline {
142 background:var(--red); color:#fff; border-color:var(--red);
143 }
144
145 /* result state */
146 .rsvp-result {
147 display:none; text-align:center; padding:24px;
148 }
149 .rsvp-result.show { display:block; }
150 .rsvp-result .result-emoji { font-size:56px; margin-bottom:12px; }
151 .rsvp-result h3 {
152 font-family:'Cormorant Garamond',serif;
153 font-size:24px; font-weight:700; margin-bottom:8px;
154 }
155 .rsvp-result p { font-size:14px; color:var(--text-mid); line-height:1.6; }
156 .result-accepted h3 { color:var(--green); }
157 .result-declined h3 { color:var(--red); }
158
159 /* loading state */
160 .rsvp-loading {
161 display:none; text-align:center; padding:32px;
162 }
163 .rsvp-loading.show { display:block; }
164 .spinner {
165 width:36px; height:36px; margin:0 auto 12px;
166 border:3px solid var(--border);
167 border-top-color:var(--rose); border-radius:50%;
168 animation:spin .7s linear infinite;
169 }
170 @keyframes spin { to { transform:rotate(360deg); } }
171
172 /* error state */
173 .rsvp-error {
174 display:none; text-align:center; padding:24px;
175 }
176 .rsvp-error.show { display:block; }
177 .rsvp-error .error-emoji { font-size:48px; margin-bottom:12px; }
178 .rsvp-error h3 {
179 font-family:'Cormorant Garamond',serif;
180 font-size:22px; color:var(--red); margin-bottom:8px;
181 }
182 .rsvp-error p { font-size:13px; color:var(--text-mid); }
183
184 /* footer */
185 .rsvp-footer {
186 background:var(--cream); border-top:1px solid var(--border-light);
187 padding:14px 32px; text-align:center;
188 font-size:12px; color:var(--text-light);
189 }
190
191 /* already responded badge */
192 .already-badge {
193 display:inline-flex; align-items:center; gap:6px;
194 padding:6px 14px; border-radius:999px;
195 font-size:12px; font-weight:600; margin-bottom:16px;
196 }
197 .already-accepted { background:var(--green-light); color:var(--green); }
198 .already-declined { background:var(--red-light); color:var(--red); }
199 .already-pending { background:#fff4e0; color:#c87f2a; }
200 </style>
201</head>
202<body>
203
204<div class="rsvp-wrapper">
205
206 <!-- Logo -->
207 <div class="rsvp-logo">
208 <div class="mark">
209 <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
210 <path d="M12 21.593c-5.63-5.539-11-10.297-11-14.402C1 3.402 4.248 1 7.5 1 9.849 1 11.271 2.007 12 3.011 12.729 2.007 14.151 1 16.5 1 19.752 1 23 3.402 23 7.191c0 4.105-5.37 8.863-11 14.402z"/>
211 </svg>
212 </div>
213 <h1>Wedding Planner</h1>
214 </div>
215
216 <!-- Card -->
217 <div class="rsvp-card">
218
219 <!-- Header -->
220 <div class="rsvp-header">
221 <div class="emoji">๐Ÿ’Œ</div>
222 <h2>Wedding Invitation</h2>
223 <p>You have been invited โ€” please let us know if you can attend</p>
224 </div>
225
226 <!-- Loading -->
227 <div class="rsvp-loading" id="loadingState">
228 <div class="rsvp-body">
229 <div class="spinner"></div>
230 <p style="color:var(--text-light);font-size:14px;">Loading your invitation...</p>
231 </div>
232 </div>
233
234 <!-- Error -->
235 <div class="rsvp-error" id="errorState">
236 <div class="rsvp-body">
237 <div class="error-emoji">โš ๏ธ</div>
238 <h3>Invalid Link</h3>
239 <p id="errorMsg">This RSVP link is invalid or has expired. Please contact the wedding organizer.</p>
240 </div>
241 </div>
242
243 <!-- Main RSVP form -->
244 <div class="rsvp-body" id="rsvpForm" style="display:none;">
245 <div class="gold-rule"></div>
246
247 <!-- existing response badge -->
248 <div id="alreadyBadge" style="display:none;"></div>
249
250 <!-- guest greeting -->
251 <div class="guest-greeting" id="guestGreeting"></div>
252
253 <!-- event info -->
254 <div class="event-box" id="eventBox"></div>
255
256 <!-- action buttons -->
257 <div class="rsvp-actions" id="rsvpActions">
258 <button class="rsvp-btn rsvp-btn-accept" id="btnAccept" onclick="submitRsvp('accepted')">
259 <span class="btn-icon">โœ…</span>
260 <span>I'll be there!</span>
261 </button>
262 <button class="rsvp-btn rsvp-btn-decline" id="btnDecline" onclick="submitRsvp('declined')">
263 <span class="btn-icon">โŒ</span>
264 <span>Can't make it</span>
265 </button>
266 </div>
267
268 <p style="font-size:12px;color:var(--text-light);text-align:center;">
269 You can change your response at any time by using the same link from your email.
270 </p>
271 </div>
272
273 <!-- Result after submitting -->
274 <div class="rsvp-result" id="rsvpResult">
275 <div class="rsvp-body">
276 <div class="result-emoji" id="resultEmoji"></div>
277 <h3 id="resultTitle"></h3>
278 <p id="resultMsg"></p>
279 </div>
280 </div>
281
282 <div class="rsvp-footer">
283 Wedding Planner ยท Sent with love ๐Ÿ’Œ
284 </div>
285 </div>
286
287</div>
288
289<script>
290 const API = 'http://localhost:3000';
291
292 // โ”€โ”€ Parse URL params โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
293 const params = new URLSearchParams(window.location.search);
294 const guestId = params.get('guest');
295 const eventId = params.get('event');
296 const token = params.get('token');
297 const action = params.get('action'); // pre-filled from email link
298
299 // โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
300 let guestData = null;
301 let existingRsvp = null;
302
303 // โ”€โ”€ On load โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
304 window.addEventListener('DOMContentLoaded', async () => {
305 if (!guestId || !eventId || !token) {
306 showError('Missing required parameters. Please use the link from your email.');
307 return;
308 }
309 showLoading(true);
310 await loadInvitation();
311 });
312
313 async function loadInvitation() {
314 try {
315 // Check for existing RSVP
316 const rsvpRes = await fetch(`${API}/api/rsvp?guest_id=${guestId}&event_id=${eventId}`);
317 if (rsvpRes.ok) {
318 existingRsvp = await rsvpRes.json();
319 }
320
321 // We'll get guest + event info from the RSVP endpoint by doing a dry run
322 // For now build the form and auto-submit if action is in URL
323 showLoading(false);
324 showForm();
325
326 // Auto-submit if action came from email link
327 if (action === 'accepted' || action === 'declined') {
328 await submitRsvp(action, true);
329 }
330
331 } catch (err) {
332 showError('Could not load invitation. Please check your connection.');
333 }
334 }
335
336 // โ”€โ”€ Submit RSVP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
337 async function submitRsvp(status, auto = false) {
338 disableButtons();
339 if (!auto) showLoading(true);
340
341 try {
342 const res = await fetch(`${API}/api/rsvp`, {
343 method: 'POST',
344 headers: { 'Content-Type': 'application/json' },
345 body: JSON.stringify({
346 guest_id: guestId,
347 event_id: eventId,
348 token,
349 status
350 })
351 });
352
353 const data = await res.json();
354 showLoading(false);
355
356 if (!res.ok) {
357 showError(data.error || 'Could not submit your response.');
358 return;
359 }
360
361 // Show result
362 const guest = data.guest;
363 const accepted = status === 'accepted';
364 showResult({
365 emoji: accepted ? '๐ŸŽ‰' : '๐Ÿ˜ข',
366 title: accepted ? 'See you there!' : 'We\'ll miss you!',
367 msg: accepted
368 ? `Thank you, ${guest ? guest.first_name : 'Guest'}! Your attendance for <strong>${guest ? guest.event_type : 'the event'}</strong> on <strong>${guest ? guest.date : ''}</strong> has been confirmed. We can't wait to celebrate with you! ๐Ÿฅ‚`
369 : `Thank you for letting us know, ${guest ? guest.first_name : 'Guest'}. We're sorry you won't be able to make it to <strong>${guest ? guest.event_type : 'the event'}</strong>, but we appreciate your response.`,
370 type: accepted ? 'accepted' : 'declined'
371 });
372
373 } catch (err) {
374 showLoading(false);
375 showError('Connection error. Please try again.');
376 }
377 }
378
379 // โ”€โ”€ UI Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
380 function showLoading(show) {
381 document.getElementById('loadingState').classList.toggle('show', show);
382 document.getElementById('rsvpForm').style.display = show ? 'none' : 'block';
383 }
384
385 function showForm() {
386 document.getElementById('loadingState').classList.remove('show');
387 document.getElementById('rsvpForm').style.display = 'block';
388
389 // Show greeting
390 document.getElementById('guestGreeting').innerHTML =
391 `Dear <strong>Guest</strong>,<br>You have been invited to a special event. Please confirm your attendance below.`;
392
393 // Show event info placeholder (will be filled after submit response)
394 document.getElementById('eventBox').innerHTML = `
395 <div class="ev-label">Event Details</div>
396 <div class="ev-type">Wedding Event</div>
397 <div class="ev-meta">
398 <span>๐Ÿ“… Check your email for date details</span>
399 </div>`;
400
401 // Show existing response badge
402 if (existingRsvp) {
403 const badge = document.getElementById('alreadyBadge');
404 badge.style.display = 'block';
405 const cls = existingRsvp.status === 'accepted' ? 'already-accepted'
406 : existingRsvp.status === 'declined' ? 'already-declined'
407 : 'already-pending';
408 const icon = existingRsvp.status === 'accepted' ? 'โœ…'
409 : existingRsvp.status === 'declined' ? 'โŒ' : 'โณ';
410 badge.innerHTML = `
411 <span class="already-badge ${cls}">
412 ${icon} You previously responded: <strong>${existingRsvp.status}</strong>
413 &nbsp;โ€” you can change your response below
414 </span>`;
415 }
416 }
417
418 function showError(msg) {
419 document.getElementById('loadingState').classList.remove('show');
420 document.getElementById('rsvpForm').style.display = 'none';
421 document.getElementById('rsvpResult').classList.remove('show');
422 const err = document.getElementById('errorState');
423 err.classList.add('show');
424 document.getElementById('errorMsg').textContent = msg;
425 }
426
427 function showResult({ emoji, title, msg, type }) {
428 document.getElementById('rsvpForm').style.display = 'none';
429 document.getElementById('loadingState').classList.remove('show');
430 const result = document.getElementById('rsvpResult');
431 result.className = `rsvp-result show result-${type}`;
432 document.getElementById('resultEmoji').textContent = emoji;
433 document.getElementById('resultTitle').textContent = title;
434 document.getElementById('resultMsg').innerHTML = msg;
435 }
436
437 function disableButtons() {
438 document.getElementById('btnAccept').disabled = true;
439 document.getElementById('btnDecline').disabled = true;
440 }
441</script>
442</body>
443</html>
Note: See TracBrowser for help on using the repository browser.