| | <!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> |