partySim / templates /index.html
dpang's picture
Upload 15 files
7b84343 verified
<!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>
<!-- Upload Section -->
<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>
<!-- Loading Section -->
<div class="card hidden" id="loadingSection">
<div class="loading">
<div class="spinner"></div>
<p>Processing your guest list...</p>
</div>
</div>
<!-- Guest Selection Section -->
<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">
<!-- Guest cards will be populated here -->
</div>
<div style="text-align: center; margin-top: 30px;">
<button class="btn btn-success" id="arrangeBtn" disabled>Arrange Tables</button>
</div>
</div>
<!-- Tables Section -->
<div class="card hidden" id="tablesSection">
<h2 style="margin-bottom: 20px; color: #667eea;">Your Event Tables</h2>
<div class="tables-container" id="tablesContainer">
<!-- Tables will be populated here -->
</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 = [];
// File upload handling
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;
// Calculate number of tables needed
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';
// Create circular table visualization
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; // Radius of the table
const seatRadius = 25; // Radius of each seat
const centerX = 150;
const centerY = 150;
let tableHTML = `
<div class="circular-table">
<div class="table-center">Table ${tableNumber}</div>
`;
// Calculate positions for seats around the table
guests.forEach((guest, index) => {
const angle = (index / guests.length) * 2 * Math.PI - Math.PI / 2; // Start from top
const x = centerX + tableRadius * Math.cos(angle);
const y = centerY + tableRadius * Math.sin(angle);
// Get initials for display
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>