feat: Add complete Docker deployment with web-based setup wizard
Major additions: - Web-based setup wizard (setup.php, setup_wizard.php, setup-wizard.js) - Production Docker configuration (docker-compose.prod.yml, .env.production) - Database initialization SQL files (deploy/init_settings.sql) - Template builder system with drag-and-drop UI - Advanced features (OAuth, CDN, enhanced analytics, monetization) - Comprehensive documentation (deployment guides, quick start, feature docs) - Design system with accessibility and responsive layout - Deployment automation scripts (deploy.ps1, generate-secrets.ps1) Setup wizard allows customization of: - Platform name and branding - Domain configuration - Membership tiers and pricing - Admin credentials - Feature toggles Database includes 270+ tables for complete video streaming platform with advanced features for analytics, moderation, template building, and monetization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
423
f_scripts/fe/js/setup-wizard.js
Normal file
423
f_scripts/fe/js/setup-wizard.js
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* EasyStream Setup Wizard - Frontend Logic
|
||||
* Handles wizard navigation, form validation, and AJAX submissions
|
||||
*/
|
||||
|
||||
let currentStep = 1;
|
||||
const totalSteps = 9;
|
||||
let setupData = {};
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateProgressBar();
|
||||
attachEventListeners();
|
||||
loadSavedData();
|
||||
});
|
||||
|
||||
function attachEventListeners() {
|
||||
// Color picker updates
|
||||
document.getElementById('primaryColor').addEventListener('input', function(e) {
|
||||
document.getElementById('primaryColorHex').textContent = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById('secondaryColor').addEventListener('input', function(e) {
|
||||
document.getElementById('secondaryColorHex').textContent = e.target.value;
|
||||
});
|
||||
|
||||
// Save form data on input
|
||||
const inputs = document.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('change', saveFormData);
|
||||
});
|
||||
}
|
||||
|
||||
function changeStep(direction) {
|
||||
const nextStep = currentStep + direction;
|
||||
|
||||
// Validate before moving forward
|
||||
if (direction > 0) {
|
||||
if (!validateCurrentStep()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for certain steps
|
||||
if (currentStep === 7 && direction > 0) {
|
||||
// Review step - show summary and install
|
||||
showReviewSummary();
|
||||
startInstallation();
|
||||
}
|
||||
|
||||
if (nextStep >= 1 && nextStep <= totalSteps) {
|
||||
// Hide current step
|
||||
document.getElementById(`step${currentStep}`).classList.remove('active');
|
||||
|
||||
// Show next step
|
||||
currentStep = nextStep;
|
||||
document.getElementById(`step${currentStep}`).classList.add('active');
|
||||
|
||||
// Update progress bar
|
||||
updateProgressBar();
|
||||
|
||||
// Update button visibility
|
||||
updateButtons();
|
||||
|
||||
// Scroll to top
|
||||
document.querySelector('.wizard-body').scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
const progress = ((currentStep - 1) / (totalSteps - 1)) * 100;
|
||||
document.getElementById('progressBar').style.width = progress + '%';
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
const nextBtn = document.getElementById('nextBtn');
|
||||
|
||||
// Show/hide previous button
|
||||
prevBtn.style.display = currentStep > 1 && currentStep < 8 ? 'block' : 'none';
|
||||
|
||||
// Update next button text and visibility
|
||||
if (currentStep === 1) {
|
||||
nextBtn.textContent = 'Get Started →';
|
||||
} else if (currentStep === 7) {
|
||||
nextBtn.textContent = 'Install Now →';
|
||||
} else if (currentStep === 8 || currentStep === 9) {
|
||||
nextBtn.style.display = 'none';
|
||||
} else {
|
||||
nextBtn.textContent = 'Next →';
|
||||
}
|
||||
}
|
||||
|
||||
function validateCurrentStep() {
|
||||
clearAlert();
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
// Welcome page, no validation needed
|
||||
return true;
|
||||
|
||||
case 2:
|
||||
// Platform configuration
|
||||
const platformName = document.getElementById('platformName').value.trim();
|
||||
const domainName = document.getElementById('domainName').value.trim();
|
||||
const contactEmail = document.getElementById('contactEmail').value.trim();
|
||||
|
||||
if (!platformName) {
|
||||
showAlert('error', 'Please enter a platform name');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!domainName) {
|
||||
showAlert('error', 'Please enter a domain name');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!contactEmail || !isValidEmail(contactEmail)) {
|
||||
showAlert('error', 'Please enter a valid contact email');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case 3:
|
||||
// Branding, no strict validation
|
||||
return true;
|
||||
|
||||
case 4:
|
||||
// Membership tiers
|
||||
const tier1Name = document.getElementById('tier1Name').value.trim();
|
||||
const tier2Name = document.getElementById('tier2Name').value.trim();
|
||||
const tier3Name = document.getElementById('tier3Name').value.trim();
|
||||
|
||||
if (!tier1Name || !tier2Name || !tier3Name) {
|
||||
showAlert('error', 'Please enter names for all membership tiers');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case 5:
|
||||
// Admin account
|
||||
const username = document.getElementById('adminUsername').value.trim();
|
||||
const email = document.getElementById('adminEmail').value.trim();
|
||||
const password = document.getElementById('adminPassword').value;
|
||||
const passwordConfirm = document.getElementById('adminPasswordConfirm').value;
|
||||
|
||||
if (!username || username.length < 4) {
|
||||
showAlert('error', 'Username must be at least 4 characters');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
||||
showAlert('error', 'Username can only contain letters, numbers, and underscores');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!email || !isValidEmail(email)) {
|
||||
showAlert('error', 'Please enter a valid admin email');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
showAlert('error', 'Password must be at least 8 characters long');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/[0-9]/.test(password)) {
|
||||
showAlert('error', 'Password must contain uppercase, lowercase, and numbers');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
showAlert('error', 'Passwords do not match');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case 6:
|
||||
// Features, no strict validation
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
function saveFormData() {
|
||||
// Save to localStorage for recovery
|
||||
const formData = collectFormData();
|
||||
localStorage.setItem('easystreamSetup', JSON.stringify(formData));
|
||||
}
|
||||
|
||||
function loadSavedData() {
|
||||
// Load from localStorage if available
|
||||
const saved = localStorage.getItem('easystreamSetup');
|
||||
if (saved) {
|
||||
try {
|
||||
const data = JSON.parse(saved);
|
||||
// Populate form fields
|
||||
Object.keys(data).forEach(key => {
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = data[key];
|
||||
} else {
|
||||
element.value = data[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load saved data:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectFormData() {
|
||||
const formData = {};
|
||||
|
||||
// Platform config
|
||||
formData.platformName = document.getElementById('platformName').value;
|
||||
formData.platformTagline = document.getElementById('platformTagline').value;
|
||||
formData.domainName = document.getElementById('domainName').value;
|
||||
formData.contactEmail = document.getElementById('contactEmail').value;
|
||||
formData.timezone = document.getElementById('timezone').value;
|
||||
|
||||
// Branding
|
||||
formData.primaryColor = document.getElementById('primaryColor').value;
|
||||
formData.secondaryColor = document.getElementById('secondaryColor').value;
|
||||
formData.defaultTheme = document.getElementById('defaultTheme').value;
|
||||
formData.enableTheming = document.getElementById('enableTheming').checked;
|
||||
|
||||
// Membership tiers
|
||||
formData.tier1Name = document.getElementById('tier1Name').value;
|
||||
formData.tier1Upload = document.getElementById('tier1Upload').value;
|
||||
formData.tier1Storage = document.getElementById('tier1Storage').value;
|
||||
|
||||
formData.tier2Name = document.getElementById('tier2Name').value;
|
||||
formData.tier2Upload = document.getElementById('tier2Upload').value;
|
||||
formData.tier2Storage = document.getElementById('tier2Storage').value;
|
||||
formData.tier2Price = document.getElementById('tier2Price').value;
|
||||
|
||||
formData.tier3Name = document.getElementById('tier3Name').value;
|
||||
formData.tier3Upload = document.getElementById('tier3Upload').value;
|
||||
formData.tier3Storage = document.getElementById('tier3Storage').value;
|
||||
formData.tier3Price = document.getElementById('tier3Price').value;
|
||||
|
||||
// Admin account
|
||||
formData.adminUsername = document.getElementById('adminUsername').value;
|
||||
formData.adminEmail = document.getElementById('adminEmail').value;
|
||||
formData.adminPassword = document.getElementById('adminPassword').value;
|
||||
formData.adminPasswordConfirm = document.getElementById('adminPasswordConfirm').value;
|
||||
formData.adminDisplayName = document.getElementById('adminDisplayName').value;
|
||||
|
||||
// Features
|
||||
formData.enableRegistration = document.getElementById('enableRegistration').checked;
|
||||
formData.enableEmailVerification = document.getElementById('enableEmailVerification').checked;
|
||||
formData.enableLiveStreaming = document.getElementById('enableLiveStreaming').checked;
|
||||
formData.enableComments = document.getElementById('enableComments').checked;
|
||||
formData.enableDownloads = document.getElementById('enableDownloads').checked;
|
||||
formData.enableMonetization = document.getElementById('enableMonetization').checked;
|
||||
formData.enableTemplateBuilder = document.getElementById('enableTemplateBuilder').checked;
|
||||
formData.enableAnalytics = document.getElementById('enableAnalytics').checked;
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
function showReviewSummary() {
|
||||
const data = collectFormData();
|
||||
const summary = document.getElementById('reviewSummary');
|
||||
|
||||
summary.innerHTML = `
|
||||
<div style="margin-bottom: 24px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 12px;">Platform Configuration</h3>
|
||||
<p><strong>Name:</strong> ${data.platformName}</p>
|
||||
<p><strong>Domain:</strong> ${data.domainName}</p>
|
||||
<p><strong>Email:</strong> ${data.contactEmail}</p>
|
||||
<p><strong>Timezone:</strong> ${data.timezone}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 24px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 12px;">Membership Tiers</h3>
|
||||
<p><strong>${data.tier1Name}:</strong> ${data.tier1Upload}MB upload, ${data.tier1Storage}GB storage</p>
|
||||
<p><strong>${data.tier2Name}:</strong> ${data.tier2Upload}MB upload, ${data.tier2Storage}GB storage ($${data.tier2Price}/month)</p>
|
||||
<p><strong>${data.tier3Name}:</strong> ${data.tier3Upload}MB upload, ${data.tier3Storage}GB storage ($${data.tier3Price}/month)</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 24px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 12px;">Admin Account</h3>
|
||||
<p><strong>Username:</strong> ${data.adminUsername}</p>
|
||||
<p><strong>Email:</strong> ${data.adminEmail}</p>
|
||||
<p><strong>Display Name:</strong> ${data.adminDisplayName || 'Administrator'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="color: #667eea; margin-bottom: 12px;">Enabled Features</h3>
|
||||
<p>${data.enableRegistration ? '✓' : '✗'} User Registration</p>
|
||||
<p>${data.enableLiveStreaming ? '✓' : '✗'} Live Streaming</p>
|
||||
<p>${data.enableComments ? '✓' : '✗'} Video Comments</p>
|
||||
<p>${data.enableMonetization ? '✓' : '✗'} Monetization</p>
|
||||
<p>${data.enableTemplateBuilder ? '✓' : '✗'} Template Builder</p>
|
||||
<p>${data.enableAnalytics ? '✓' : '✗'} Analytics</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function startInstallation() {
|
||||
// Move to installation step
|
||||
document.getElementById('step7').classList.remove('active');
|
||||
currentStep = 8;
|
||||
document.getElementById('step8').classList.add('active');
|
||||
updateProgressBar();
|
||||
updateButtons();
|
||||
|
||||
const data = collectFormData();
|
||||
setupData = data;
|
||||
|
||||
try {
|
||||
// Step 1: Save configuration
|
||||
await installStep('Saving configuration...', async () => {
|
||||
const response = await fetch('setup.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ action: 'save_configuration', ...data })
|
||||
});
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
// Step 2: Create admin user
|
||||
await installStep('Creating admin account...', async () => {
|
||||
const response = await fetch('setup.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ action: 'create_admin', ...data })
|
||||
});
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
// Step 3: Finalize setup
|
||||
await installStep('Finalizing setup...', async () => {
|
||||
const response = await fetch('setup.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ action: 'finalize', ...data })
|
||||
});
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
// Success! Move to final step
|
||||
document.getElementById('step8').classList.remove('active');
|
||||
currentStep = 9;
|
||||
document.getElementById('step9').classList.add('active');
|
||||
updateProgressBar();
|
||||
|
||||
// Populate final details
|
||||
document.getElementById('finalPlatformName').textContent = data.platformName;
|
||||
document.getElementById('finalDomainName').textContent = data.domainName;
|
||||
document.getElementById('finalAdminUsername').textContent = data.adminUsername;
|
||||
|
||||
// Clear saved data
|
||||
localStorage.removeItem('easystreamSetup');
|
||||
|
||||
} catch (error) {
|
||||
document.getElementById('installStatus').textContent = 'Installation Failed';
|
||||
document.getElementById('installStep').innerHTML = `
|
||||
<div style="color: #e53e3e; padding: 20px; background: #fed7d7; border-radius: 8px;">
|
||||
<strong>Error:</strong> ${error.message}
|
||||
<br><br>
|
||||
<button onclick="location.reload()" class="btn btn-primary">Try Again</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function installStep(message, action) {
|
||||
document.getElementById('installStep').textContent = message;
|
||||
|
||||
// Add progress item
|
||||
const progress = document.getElementById('installProgress');
|
||||
const item = document.createElement('div');
|
||||
item.style.cssText = 'padding: 12px; background: #f7fafc; border-radius: 6px; margin-bottom: 8px; display: flex; align-items: center;';
|
||||
item.innerHTML = `<span class="spinner" style="width: 16px; height: 16px; margin-right: 12px; border-width: 2px;"></span><span>${message}</span>`;
|
||||
progress.appendChild(item);
|
||||
|
||||
try {
|
||||
const result = await action();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Unknown error occurred');
|
||||
}
|
||||
|
||||
// Update to success
|
||||
item.innerHTML = `<span style="color: #48bb78; font-size: 20px; margin-right: 12px;">✓</span><span>${message} <strong style="color: #48bb78;">Done!</strong></span>`;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
item.innerHTML = `<span style="color: #e53e3e; font-size: 20px; margin-right: 12px;">✗</span><span>${message} <strong style="color: #e53e3e;">Failed!</strong></span>`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.className = `alert alert-${type} show`;
|
||||
alert.textContent = message;
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
alert.classList.remove('show');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function clearAlert() {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.classList.remove('show');
|
||||
}
|
||||
Reference in New Issue
Block a user