Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"/> | |
| <title>Contact Session | VCF Session</title> | |
| <style> | |
| :root { | |
| --primary: #2563eb; | |
| --primary-hover: #1d4ed8; | |
| --success: #059669; | |
| --error: #dc2626; | |
| --warning: #d97706; | |
| --bg-primary: #ffffff; | |
| --bg-secondary: #f8fafc; | |
| --bg-card: #ffffff; | |
| --text-primary: #0f172a; | |
| --text-secondary: #64748b; | |
| --text-muted: #94a3b8; | |
| --border: #e2e8f0; | |
| --radius: 8px; | |
| --radius-lg: 12px; | |
| --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| --transition: all .2s cubic-bezier(.4,0,.2,1); | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --primary: #3b82f6; | |
| --primary-hover: #2563eb; | |
| --bg-primary: #0f172a; | |
| --bg-secondary: #1e293b; | |
| --bg-card: #1e293b; | |
| --text-primary: #f1f5f9; | |
| --text-secondary: #cbd5e1; | |
| --text-muted: #64748b; | |
| --border: #334155; | |
| } | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { | |
| height: 100%; | |
| font-family: var(--font-sans); | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| line-height: 1.6; | |
| } | |
| .container { min-height: 100vh; padding: 1rem; background: var(--bg-secondary); } | |
| .header { text-align: center; padding: 2rem 0; } | |
| .session-title { font-size: 1.875rem; font-weight: 700; margin-bottom: .5rem; } | |
| .session-status { display: flex; align-items: center; justify-content: center; gap: .5rem; color: var(--text-secondary); } | |
| .status-indicator { width: 8px; height: 8px; border-radius: 50%; background: var(--success); animation: pulse 2s infinite; } | |
| .status-indicator.expired { background: var(--error); animation: none; } | |
| @keyframes pulse { 50% { opacity: .5; } } | |
| .countdown { font-weight: 600; color: var(--primary); } | |
| .countdown.warning { color: var(--warning); } | |
| .countdown.expired { color: var(--error); } | |
| .main-content { max-width: 600px; margin: 0 auto; display: grid; gap: 2rem; } | |
| .card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: 0 4px 6px -1px rgb(0 0 0 / .1); padding: 2rem; } | |
| .card-header { margin-bottom: 1.5rem; } | |
| .card-title { font-size: 1.25rem; font-weight: 600; } | |
| .card-description { color: var(--text-secondary); font-size: .875rem; } | |
| .form-group { margin-bottom: 1rem; } | |
| .form-label { display: block; font-size: .875rem; font-weight: 600; margin-bottom: .5rem; } | |
| .form-input { width: 100%; padding: .75rem 1rem; border: 1px solid var(--border); border-radius: var(--radius); font-size: 1rem; background: var(--bg-primary); color: var(--text-primary); } | |
| .form-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgb(37 99 235 / .1); } | |
| .form-input.error { border-color: var(--error); } | |
| .error-text { color: var(--error); font-size: .75rem; margin-top: .25rem; display: none; } | |
| .add-btn { width: 100%; padding: .875rem 1.5rem; background: var(--primary); color: #fff; border: none; border-radius: var(--radius); font-size: 1rem; font-weight: 600; cursor: pointer; transition: var(--transition); } | |
| .add-btn:hover:not(:disabled) { background: var(--primary-hover); } | |
| .add-btn:disabled { background: var(--text-muted); cursor: not-allowed; } | |
| .success-message { background: #f0fdf4; border: 1px solid #bbf7d0; color: var(--success); padding: .75rem 1rem; border-radius: var(--radius); margin-bottom: 1rem; display: none; } | |
| .download-section { text-align: center; } | |
| .download-btn { display: inline-flex; align-items: center; gap: .5rem; padding: .875rem 2rem; background: var(--success); color: #fff; text-decoration: none; border-radius: var(--radius); font-weight: 600; } | |
| .download-btn:hover { background: #047857; } | |
| .footer { text-align: center; padding: 2rem 1rem; color: var(--text-muted); font-size: .875rem; } | |
| .footer a { color: var(--primary); text-decoration: none; } | |
| .footer a:hover { text-decoration: underline; } | |
| .session-expired { text-align: center; padding: 3rem 1rem; } | |
| .expired-icon { width: 64px; height: 64px; background: var(--error); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1.5rem; color: #fff; font-size: 2rem; } | |
| @media (max-width: 640px) { .container { padding: .5rem; } .card { padding: 1.5rem; } } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1 id="sessionTitle" class="session-title">Loading…</h1> | |
| <div class="session-status"> | |
| <div id="statusIndicator" class="status-indicator"></div> | |
| <span>Session expires in <span id="countdown" class="countdown">--:--</span></span> | |
| </div> | |
| </div> | |
| <div class="main-content"> | |
| <!-- Add-Contact Card --> | |
| <div id="contactForm" class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title">Add Your Contact</h2> | |
| <p class="card-description">Share your contact information with other participants</p> | |
| </div> | |
| <div id="successMessage" class="success-message">Contact added!</div> | |
| <form id="addContactForm"> | |
| <div class="form-group"> | |
| <label for="contactName" class="form-label">Full Name *</label> | |
| <input id="contactName" type="text" class="form-input" placeholder="John Doe" required autocomplete="name"> | |
| <div id="nameError" class="error-text"></div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="contactPhone" class="form-label">Phone Number *</label> | |
| <input id="contactPhone" type="tel" class="form-input" placeholder="+1234567890" required autocomplete="tel"> | |
| <div id="phoneError" class="error-text"></div> | |
| </div> | |
| <button type="submit" id="addBtn" class="add-btn">Add Contact</button> | |
| </form> | |
| </div> | |
| <!-- Expired Notice --> | |
| <div id="expiredCard" class="card" style="display:none;"> | |
| <div class="session-expired"> | |
| <div class="expired-icon">✖</div> | |
| <h2 class="card-title">Session Expired</h2> | |
| <p class="card-description">This session has ended. You may still download the collected contacts.</p> | |
| </div> | |
| </div> | |
| <!-- Download Section --> | |
| <div id="downloadSection" class="card" style="display:none;"> | |
| <div class="download-section"> | |
| <a id="downloadBtn" class="download-btn" href="">Download VCF File</a> | |
| <p class="card-description">Import the file into any contacts app</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| Join our WhatsApp group: | |
| <a href="https://chat.whatsapp.com/JRe69IJCINM3qYX3gM8MI9?mode=ems_copy_c" target="_blank" rel="noopener">VCF GROUP</a> | |
| </div> | |
| </div> | |
| <script> | |
| const sessionId = window.location.pathname.split('/').pop(); | |
| if (!sessionId) window.location.href = '/'; | |
| const sessionTitle = document.getElementById('sessionTitle'); | |
| const statusInd = document.getElementById('statusIndicator'); | |
| const countdown = document.getElementById('countdown'); | |
| const contactForm = document.getElementById('contactForm'); | |
| const expiredCard = document.getElementById('expiredCard'); | |
| const downloadSect = document.getElementById('downloadSection'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const addForm = document.getElementById('addContactForm'); | |
| const successMsg = document.getElementById('successMessage'); | |
| const nameInput = document.getElementById('contactName'); | |
| const phoneInput = document.getElementById('contactPhone'); | |
| const nameError = document.getElementById('nameError'); | |
| const phoneError = document.getElementById('phoneError'); | |
| let sessionData = null; | |
| let isExpired = false; | |
| let countdownTimer = null; | |
| const sanitize = str => str.replace(/[\x00-\x1F\x7F]/g, ' ').trim(); | |
| async function loadSession() { | |
| const res = await fetch(`/api/session/${sessionId}`); | |
| if (!res.ok) { document.body.innerHTML = `<div class="card empty-state"><h2>Session Not Found</h2><a href="/">← Home</a></div>`; return; } | |
| sessionData = await res.json(); | |
| sessionTitle.textContent = sessionData.name; | |
| startCountdown(); | |
| } | |
| loadSession(); | |
| function startCountdown() { | |
| function tick() { | |
| const left = sessionData.expiresAt - Date.now(); | |
| if (left <= 0) { | |
| isExpired = true; | |
| countdown.textContent = 'Expired'; | |
| countdown.classList.add('expired'); | |
| statusInd.classList.add('expired'); | |
| contactForm.style.display = 'none'; | |
| expiredCard.style.display = 'block'; | |
| downloadSect.style.display = 'block'; | |
| downloadBtn.href = `/api/session/${sessionId}/contacts.vcf`; | |
| clearInterval(countdownTimer); | |
| return; | |
| } | |
| const m = String(Math.floor(left / 60000)).padStart(2, '0'); | |
| const s = String(Math.floor((left % 60000) / 1000)).padStart(2, '0'); | |
| countdown.textContent = `${m}:${s}`; | |
| if (left < 300000) countdown.classList.add('warning'); | |
| } | |
| tick(); | |
| countdownTimer = setInterval(tick, 1000); | |
| } | |
| function showError(el, msg) { el.classList.add('error'); el.nextElementSibling.textContent = msg; el.nextElementSibling.style.display = 'block'; } | |
| function hideError(el) { el.classList.remove('error'); el.nextElementSibling.style.display = 'none'; } | |
| addForm.addEventListener('submit', async e => { | |
| e.preventDefault(); | |
| if (isExpired) return; | |
| const name = sanitize(nameInput.value); | |
| const phone = sanitize(phoneInput.value); | |
| let ok = true; | |
| if (!name) { showError(nameInput, 'Name required'); ok = false; } else hideError(nameInput); | |
| if (!phone) { showError(phoneInput, 'Phone required'); ok = false; } else hideError(phoneInput); | |
| if (!ok) return; | |
| const res = await fetch(`/api/session/${sessionId}/contact`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ name, phone }) | |
| }); | |
| if (res.ok) { | |
| successMsg.style.display = 'block'; | |
| addForm.reset(); | |
| await loadSession(); | |
| setTimeout(() => successMsg.style.display = 'none', 3000); | |
| } else { | |
| const err = await res.json(); | |
| alert(err.error || 'Failed to add contact'); | |
| } | |
| }); | |
| window.addEventListener('beforeunload', () => clearInterval(countdownTimer)); | |
| </script> | |
| </body> | |
| </html> | |