| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Party Planner - Guest Table Arranger</title> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| min-height: 100vh; |
| color: #333; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| padding: 20px; |
| } |
| |
| .header { |
| text-align: center; |
| color: white; |
| margin-bottom: 40px; |
| } |
| |
| .header h1 { |
| font-size: 2.5rem; |
| margin-bottom: 10px; |
| font-weight: 300; |
| } |
| |
| .header p { |
| font-size: 1.1rem; |
| opacity: 0.9; |
| } |
| |
| .card { |
| background: white; |
| border-radius: 12px; |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); |
| padding: 30px; |
| margin-bottom: 30px; |
| } |
| |
| .upload-form { |
| max-width: 500px; |
| margin: 0 auto; |
| } |
| |
| .form-group { |
| margin-bottom: 20px; |
| } |
| |
| .form-group label { |
| display: block; |
| margin-bottom: 8px; |
| font-weight: 500; |
| color: #333; |
| } |
| |
| .file-upload-container { |
| border: 2px dashed #667eea; |
| border-radius: 8px; |
| padding: 40px 20px; |
| text-align: center; |
| background: #f8f9ff; |
| transition: all 0.3s ease; |
| cursor: pointer; |
| } |
| |
| .file-upload-container:hover { |
| border-color: #5a6fd8; |
| background: #f0f2ff; |
| } |
| |
| .file-upload-container.dragover { |
| border-color: #5a6fd8; |
| background: #e8ecff; |
| } |
| |
| .file-upload-icon { |
| font-size: 3rem; |
| color: #667eea; |
| margin-bottom: 15px; |
| } |
| |
| .file-upload-text { |
| font-size: 1.1rem; |
| color: #667eea; |
| margin-bottom: 10px; |
| } |
| |
| .file-upload-hint { |
| font-size: 0.9rem; |
| color: #666; |
| } |
| |
| .file-input { |
| display: none; |
| } |
| |
| .btn { |
| background: #667eea; |
| color: white; |
| border: none; |
| padding: 12px 24px; |
| border-radius: 8px; |
| font-size: 16px; |
| font-weight: 500; |
| cursor: pointer; |
| transition: background-color 0.3s ease; |
| width: 100%; |
| } |
| |
| .btn:hover { |
| background: #5a6fd8; |
| } |
| |
| .btn:disabled { |
| background: #ccc; |
| cursor: not-allowed; |
| } |
| |
| .btn-secondary { |
| background: #6c757d; |
| } |
| |
| .btn-secondary:hover { |
| background: #545b62; |
| } |
| |
| .btn-success { |
| background: #28a745; |
| } |
| |
| .btn-success:hover { |
| background: #1e7e34; |
| } |
| |
| .guests-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
| gap: 20px; |
| margin-bottom: 30px; |
| } |
| |
| .guest-card { |
| border: 2px solid #e1e5e9; |
| border-radius: 8px; |
| padding: 20px; |
| transition: all 0.3s ease; |
| cursor: pointer; |
| } |
| |
| .guest-card:hover { |
| border-color: #667eea; |
| box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); |
| } |
| |
| .guest-card.selected { |
| border-color: #667eea; |
| background: #f8f9ff; |
| } |
| |
| .guest-checkbox { |
| margin-right: 12px; |
| transform: scale(1.2); |
| } |
| |
| .guest-name { |
| font-weight: 600; |
| font-size: 1.1rem; |
| margin-bottom: 8px; |
| color: #667eea; |
| } |
| |
| .guest-title { |
| color: #666; |
| font-size: 0.9rem; |
| line-height: 1.4; |
| } |
| |
| .tables-container { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); |
| gap: 40px; |
| margin-top: 30px; |
| } |
| |
| .table-card { |
| background: #f8f9fa; |
| border-radius: 12px; |
| padding: 25px; |
| border: 2px solid #e1e5e9; |
| } |
| |
| .table-header { |
| text-align: center; |
| margin-bottom: 30px; |
| padding-bottom: 15px; |
| border-bottom: 2px solid #667eea; |
| } |
| |
| .table-title { |
| font-size: 1.3rem; |
| font-weight: 600; |
| color: #667eea; |
| margin-bottom: 5px; |
| } |
| |
| .table-count { |
| color: #666; |
| font-size: 0.9rem; |
| } |
| |
| .circular-table { |
| position: relative; |
| width: 300px; |
| height: 300px; |
| margin: 0 auto 30px; |
| border-radius: 50%; |
| background: linear-gradient(145deg, #e6e6e6, #ffffff); |
| box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); |
| border: 3px solid #667eea; |
| } |
| |
| .table-center { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| width: 60px; |
| height: 60px; |
| background: #667eea; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: white; |
| font-weight: 600; |
| font-size: 0.9rem; |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); |
| } |
| |
| .seat { |
| position: absolute; |
| width: 50px; |
| height: 50px; |
| background: #28a745; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: white; |
| font-size: 0.7rem; |
| font-weight: 600; |
| text-align: center; |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); |
| transition: all 0.3s ease; |
| cursor: pointer; |
| } |
| |
| .seat:hover { |
| transform: scale(1.1); |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); |
| } |
| |
| .seat-info { |
| position: absolute; |
| bottom: -60px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: #333; |
| color: white; |
| padding: 8px 12px; |
| border-radius: 6px; |
| font-size: 0.8rem; |
| white-space: nowrap; |
| opacity: 0; |
| transition: opacity 0.3s ease; |
| pointer-events: none; |
| z-index: 10; |
| } |
| |
| .seat-info::before { |
| content: ''; |
| position: absolute; |
| top: -5px; |
| left: 50%; |
| transform: translateX(-50%); |
| border-left: 5px solid transparent; |
| border-right: 5px solid transparent; |
| border-bottom: 5px solid #333; |
| } |
| |
| .seat:hover .seat-info { |
| opacity: 1; |
| } |
| |
| .stats-panel { |
| background: #667eea; |
| color: white; |
| padding: 20px; |
| border-radius: 12px; |
| margin-bottom: 30px; |
| text-align: center; |
| } |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
| gap: 20px; |
| margin-top: 15px; |
| } |
| |
| .stat-item { |
| text-align: center; |
| } |
| |
| .stat-number { |
| font-size: 2rem; |
| font-weight: 600; |
| margin-bottom: 5px; |
| } |
| |
| .stat-label { |
| font-size: 0.9rem; |
| opacity: 0.9; |
| } |
| |
| .loading { |
| text-align: center; |
| padding: 40px; |
| color: #666; |
| } |
| |
| .spinner { |
| border: 4px solid #f3f3f3; |
| border-top: 4px solid #667eea; |
| border-radius: 50%; |
| width: 40px; |
| height: 40px; |
| animation: spin 1s linear infinite; |
| margin: 0 auto 20px; |
| } |
| |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| .error-message { |
| background: #f8d7da; |
| color: #721c24; |
| padding: 15px; |
| border-radius: 8px; |
| margin-bottom: 20px; |
| border: 1px solid #f5c6cb; |
| } |
| |
| .success-message { |
| background: #d4edda; |
| color: #155724; |
| padding: 15px; |
| border-radius: 8px; |
| margin-bottom: 20px; |
| border: 1px solid #c3e6cb; |
| } |
| |
| .csv-format-info { |
| background: #e7f3ff; |
| border: 1px solid #b3d9ff; |
| border-radius: 8px; |
| padding: 20px; |
| margin-bottom: 20px; |
| } |
| |
| .csv-format-info h3 { |
| color: #0056b3; |
| margin-bottom: 10px; |
| } |
| |
| .csv-format-info ul { |
| margin-left: 20px; |
| color: #333; |
| } |
| |
| .csv-format-info li { |
| margin-bottom: 5px; |
| } |
| |
| .hidden { |
| display: none; |
| } |
| |
| @media (max-width: 768px) { |
| .container { |
| padding: 10px; |
| } |
| |
| .header h1 { |
| font-size: 2rem; |
| } |
| |
| .guests-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .tables-container { |
| grid-template-columns: 1fr; |
| } |
| |
| .circular-table { |
| width: 250px; |
| height: 250px; |
| } |
| |
| .seat { |
| width: 40px; |
| height: 40px; |
| font-size: 0.6rem; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="header"> |
| <h1>Party Planner</h1> |
| <p>Upload your guest list and arrange them into perfect networking tables</p> |
| </div> |
|
|
| |
| <div class="card" id="uploadSection"> |
| <div class="upload-form"> |
| <div class="csv-format-info"> |
| <h3>📋 CSV Format Requirements</h3> |
| <ul> |
| <li>Your CSV should have exactly 2 columns: name and message</li> |
| <li>The first column should contain guest names</li> |
| <li>The second column should contain descriptions, roles, or messages</li> |
| <li>Entries with blank names or descriptions will be automatically filtered out</li> |
| <li>No limit on the number of guests - tables will be created as needed</li> |
| </ul> |
| </div> |
|
|
| <div class="form-group"> |
| <div class="file-upload-container" id="fileUploadContainer"> |
| <div class="file-upload-icon">📁</div> |
| <div class="file-upload-text">Click to upload or drag and drop</div> |
| <div class="file-upload-hint">CSV files only</div> |
| <input type="file" id="csvFile" class="file-input" accept=".csv" /> |
| </div> |
| </div> |
|
|
| <button class="btn" id="uploadBtn" disabled>Upload Guest List</button> |
| </div> |
| </div> |
|
|
| |
| <div class="card hidden" id="loadingSection"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| <p>Processing your guest list...</p> |
| </div> |
| </div> |
|
|
| |
| <div class="card hidden" id="guestSection"> |
| <h2 style="margin-bottom: 20px; color: #667eea;">Select Guests for Your Event</h2> |
| |
| <div class="stats-panel"> |
| <div class="stats-grid"> |
| <div class="stat-item"> |
| <div class="stat-number" id="totalGuests">0</div> |
| <div class="stat-label">Total Guests</div> |
| </div> |
| <div class="stat-item"> |
| <div class="stat-number" id="selectedGuests">0</div> |
| <div class="stat-label">Selected</div> |
| </div> |
| <div class="stat-item"> |
| <div class="stat-number" id="remainingSlots">0</div> |
| <div class="stat-label">Tables Needed</div> |
| </div> |
| </div> |
| </div> |
|
|
| <div style="text-align: center; margin-bottom: 20px;"> |
| <button class="btn btn-secondary" id="selectAllBtn" style="width: auto; margin-right: 10px;">Select All</button> |
| <button class="btn btn-secondary" id="deselectAllBtn" style="width: auto;">Deselect All</button> |
| </div> |
|
|
| <div class="guests-grid" id="guestsGrid"> |
| |
| </div> |
|
|
| <div style="text-align: center; margin-top: 30px;"> |
| <button class="btn btn-success" id="arrangeBtn" disabled>Arrange Tables</button> |
| </div> |
| </div> |
|
|
| |
| <div class="card hidden" id="tablesSection"> |
| <h2 style="margin-bottom: 20px; color: #667eea;">Your Event Tables</h2> |
| |
| <div class="tables-container" id="tablesContainer"> |
| |
| </div> |
|
|
| <div style="text-align: center; margin-top: 30px;"> |
| <button class="btn btn-secondary" id="backToGuestsBtn">Back to Guest Selection</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| let guests = []; |
| let selectedGuests = []; |
| |
| |
| const fileUploadContainer = document.getElementById('fileUploadContainer'); |
| const csvFileInput = document.getElementById('csvFile'); |
| const uploadBtn = document.getElementById('uploadBtn'); |
| |
| fileUploadContainer.addEventListener('click', () => { |
| csvFileInput.click(); |
| }); |
| |
| fileUploadContainer.addEventListener('dragover', (e) => { |
| e.preventDefault(); |
| fileUploadContainer.classList.add('dragover'); |
| }); |
| |
| fileUploadContainer.addEventListener('dragleave', () => { |
| fileUploadContainer.classList.remove('dragover'); |
| }); |
| |
| fileUploadContainer.addEventListener('drop', (e) => { |
| e.preventDefault(); |
| fileUploadContainer.classList.remove('dragover'); |
| |
| const files = e.dataTransfer.files; |
| if (files.length > 0) { |
| csvFileInput.files = files; |
| handleFileSelect(); |
| } |
| }); |
| |
| csvFileInput.addEventListener('change', handleFileSelect); |
| |
| function handleFileSelect() { |
| const file = csvFileInput.files[0]; |
| if (file) { |
| uploadBtn.disabled = false; |
| fileUploadContainer.querySelector('.file-upload-text').textContent = `Selected: ${file.name}`; |
| } else { |
| uploadBtn.disabled = true; |
| fileUploadContainer.querySelector('.file-upload-text').textContent = 'Click to upload or drag and drop'; |
| } |
| } |
| |
| uploadBtn.addEventListener('click', uploadCSV); |
| |
| async function uploadCSV() { |
| const file = csvFileInput.files[0]; |
| if (!file) return; |
| |
| const formData = new FormData(); |
| formData.append('file', file); |
| |
| showSection('loadingSection'); |
| |
| try { |
| const response = await fetch('/upload-csv', { |
| method: 'POST', |
| body: formData |
| }); |
| |
| const result = await response.json(); |
| |
| if (result.success) { |
| guests = result.guests; |
| selectedGuests = []; |
| displayGuests(); |
| showSection('guestSection'); |
| showMessage(result.message, 'success'); |
| } else { |
| showSection('uploadSection'); |
| showMessage(result.error, 'error'); |
| } |
| } catch (error) { |
| showSection('uploadSection'); |
| showMessage('Upload failed. Please try again.', 'error'); |
| } |
| } |
| |
| function displayGuests() { |
| const guestsGrid = document.getElementById('guestsGrid'); |
| guestsGrid.innerHTML = ''; |
| |
| guests.forEach(guest => { |
| const guestCard = document.createElement('div'); |
| guestCard.className = 'guest-card'; |
| guestCard.onclick = () => toggleGuestSelection(guest); |
| |
| guestCard.innerHTML = ` |
| <input type="checkbox" class="guest-checkbox" ${selectedGuests.includes(guest.id) ? 'checked' : ''}> |
| <div class="guest-name">${guest.name}</div> |
| <div class="guest-title">${guest.title}</div> |
| `; |
| |
| guestsGrid.appendChild(guestCard); |
| }); |
| |
| updateStats(); |
| } |
| |
| function toggleGuestSelection(guest) { |
| const index = selectedGuests.indexOf(guest.id); |
| if (index > -1) { |
| selectedGuests.splice(index, 1); |
| } else { |
| selectedGuests.push(guest.id); |
| } |
| |
| displayGuests(); |
| } |
| |
| function updateStats() { |
| document.getElementById('totalGuests').textContent = guests.length; |
| document.getElementById('selectedGuests').textContent = selectedGuests.length; |
| |
| |
| const numTables = Math.ceil(selectedGuests.length / 10); |
| document.getElementById('remainingSlots').textContent = numTables; |
| |
| document.getElementById('arrangeBtn').disabled = selectedGuests.length === 0; |
| } |
| |
| document.getElementById('arrangeBtn').addEventListener('click', arrangeTables); |
| |
| async function arrangeTables() { |
| const selectedGuestObjects = guests.filter(guest => selectedGuests.includes(guest.id)); |
| |
| try { |
| const response = await fetch('/arrange-tables', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| selected_guests: selectedGuestObjects |
| }) |
| }); |
| |
| const result = await response.json(); |
| |
| if (result.success) { |
| displayTables(result.tables); |
| showSection('tablesSection'); |
| } else { |
| showMessage(result.error, 'error'); |
| } |
| } catch (error) { |
| showMessage('Failed to arrange tables. Please try again.', 'error'); |
| } |
| } |
| |
| function displayTables(tables) { |
| const tablesContainer = document.getElementById('tablesContainer'); |
| tablesContainer.innerHTML = ''; |
| |
| tables.forEach((table, index) => { |
| const tableCard = document.createElement('div'); |
| tableCard.className = 'table-card'; |
| |
| |
| const circularTable = createCircularTable(table, index + 1); |
| |
| tableCard.innerHTML = ` |
| <div class="table-header"> |
| <div class="table-title">Table ${index + 1}</div> |
| <div class="table-count">${table.length} guests</div> |
| </div> |
| ${circularTable} |
| `; |
| |
| tablesContainer.appendChild(tableCard); |
| }); |
| } |
| |
| function createCircularTable(guests, tableNumber) { |
| if (guests.length === 0) { |
| return '<div class="circular-table"><div class="table-center">Empty</div></div>'; |
| } |
| |
| const tableRadius = 120; |
| const seatRadius = 25; |
| const centerX = 150; |
| const centerY = 150; |
| |
| let tableHTML = ` |
| <div class="circular-table"> |
| <div class="table-center">Table ${tableNumber}</div> |
| `; |
| |
| |
| guests.forEach((guest, index) => { |
| const angle = (index / guests.length) * 2 * Math.PI - Math.PI / 2; |
| const x = centerX + tableRadius * Math.cos(angle); |
| const y = centerY + tableRadius * Math.sin(angle); |
| |
| |
| const initials = guest.name.split(' ').map(n => n[0]).join('').toUpperCase(); |
| |
| tableHTML += ` |
| <div class="seat" style="left: ${x - seatRadius}px; top: ${y - seatRadius}px;"> |
| ${initials} |
| <div class="seat-info"> |
| <strong>${guest.name}</strong><br> |
| ${guest.title} |
| </div> |
| </div> |
| `; |
| }); |
| |
| tableHTML += '</div>'; |
| return tableHTML; |
| } |
| |
| document.getElementById('backToGuestsBtn').addEventListener('click', () => { |
| showSection('guestSection'); |
| }); |
| |
| function showSection(sectionId) { |
| document.querySelectorAll('.card').forEach(card => { |
| card.classList.add('hidden'); |
| }); |
| document.getElementById(sectionId).classList.remove('hidden'); |
| } |
| |
| function showMessage(message, type) { |
| const existingMessage = document.querySelector('.error-message, .success-message'); |
| if (existingMessage) { |
| existingMessage.remove(); |
| } |
| |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = type === 'error' ? 'error-message' : 'success-message'; |
| messageDiv.textContent = message; |
| |
| const container = document.querySelector('.container'); |
| container.insertBefore(messageDiv, container.firstChild); |
| |
| setTimeout(() => { |
| messageDiv.remove(); |
| }, 5000); |
| } |
| |
| document.getElementById('selectAllBtn').addEventListener('click', () => { |
| selectedGuests = guests.map(guest => guest.id); |
| displayGuests(); |
| }); |
| |
| document.getElementById('deselectAllBtn').addEventListener('click', () => { |
| selectedGuests = []; |
| displayGuests(); |
| }); |
| </script> |
| </body> |
| </html> |