Spaces:
Running
Running
Download index.html from SpectrumInfinite/link2flyer-si: direct link, hf CLI and curl.
- Browser
- Download file 50 kB
-
https://huggingface.co/spaces/SpectrumInfinite/link2flyer-si/resolve/main/index.html
- Command line
-
hf download hf://spaces/SpectrumInfinite/link2flyer-si/index.html
-
curl -L -o index.html https://huggingface.co/spaces/SpectrumInfinite/link2flyer-si/resolve/main/index.html
50 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FlyerForge - Create Beautiful Product Flyers</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .color-swatch { | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| } | |
| .color-swatch:hover { | |
| transform: scale(1.1); | |
| } | |
| .color-swatch.selected { | |
| border: 3px solid #333; | |
| transform: scale(1.1); | |
| } | |
| .product-placeholder { | |
| background-color: #f3f4f6; | |
| border: 2px dashed #d1d5db; | |
| border-radius: 0.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 150px; | |
| } | |
| .flyer-preview { | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| border: 1px solid #e5e7eb; | |
| min-height: 600px; | |
| } | |
| .tab-button { | |
| position: relative; | |
| padding: 0.75rem 1.5rem; | |
| font-weight: 500; | |
| color: #6b7280; | |
| } | |
| .tab-button.active { | |
| color: #3b82f6; | |
| } | |
| .tab-button.active:after { | |
| content: ''; | |
| position: absolute; | |
| bottom: -1px; | |
| left: 0; | |
| right: 0; | |
| height: 2px; | |
| background-color: #3b82f6; | |
| } | |
| .slide-enter-active, .slide-leave-active { | |
| transition: all 0.3s ease; | |
| } | |
| .slide-enter-from, .slide-leave-to { | |
| opacity: 0; | |
| transform: translateX(20px); | |
| } | |
| .fade-enter-active, .fade-leave-active { | |
| transition: opacity 0.3s ease; | |
| } | |
| .fade-enter-from, .fade-leave-to { | |
| opacity: 0; | |
| } | |
| .flyer-header, .flyer-footer { | |
| background-color: var(--theme-color, #3b82f6); | |
| color: white; | |
| } | |
| .product-card { | |
| transition: all 0.3s ease; | |
| } | |
| .product-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| } | |
| .status-badge { | |
| position: absolute; | |
| top: 0.5rem; | |
| right: 0.5rem; | |
| z-index: 10; | |
| } | |
| .editor-toolbar { | |
| position: absolute; | |
| top: -10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: white; | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 9999px; | |
| box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); | |
| display: flex; | |
| gap: 0.25rem; | |
| z-index: 20; | |
| } | |
| .editable { | |
| cursor: text; | |
| outline: 1px dashed rgba(59, 130, 246, 0.3); | |
| padding: 2px; | |
| min-height: 1em; | |
| } | |
| .editable:hover { | |
| outline: 1px dashed rgba(59, 130, 246, 0.7); | |
| background-color: rgba(59, 130, 246, 0.05); | |
| } | |
| .editable-image { | |
| position: relative; | |
| cursor: pointer; | |
| } | |
| .editable-image:hover::after { | |
| content: 'Click to change'; | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| text-align: center; | |
| padding: 2px; | |
| font-size: 12px; | |
| } | |
| .company-name { | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl" id="app"> | |
| <!-- Header --> | |
| <header class="flex justify-between items-center mb-8"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-bullhorn text-blue-500 text-2xl mr-2"></i> | |
| <h1 class="text-2xl font-bold text-gray-800">FlyerForge</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <div class="text-sm text-gray-600"> | |
| <span id="product-count">0</span>/<span id="max-products">5</span> products | |
| </div> | |
| <button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"> | |
| Upgrade Plan | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Progress Steps --> | |
| <div class="flex justify-center mb-8"> | |
| <div class="flex space-x-8"> | |
| <div class="tab-button active" data-step="1"> | |
| <span class="flex items-center justify-center w-8 h-8 bg-blue-500 text-white rounded-full mr-2">1</span> | |
| Add Products | |
| </div> | |
| <div class="tab-button" data-step="2"> | |
| <span class="flex items-center justify-center w-8 h-8 bg-gray-200 text-gray-600 rounded-full mr-2">2</span> | |
| Branding & Preview | |
| </div> | |
| <div class="tab-button" data-step="3"> | |
| <span class="flex items-center justify-center w-8 h-8 bg-gray-200 text-gray-600 rounded-full mr-2">3</span> | |
| Final Preview & Export | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 1: Add Products --> | |
| <div id="step-1" class="step-content"> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4">Add Product URLs</h2> | |
| <p class="text-gray-600 mb-6">You can share up to <span class="font-medium">5</span> product links</p> | |
| <div id="url-inputs" class="space-y-4"> | |
| <!-- URL inputs will be added here --> | |
| </div> | |
| <div class="mt-6 flex justify-end"> | |
| <button id="next-step-1" class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
| Next <i class="fas fa-arrow-right ml-2"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 2: Branding & Preview --> | |
| <div id="step-2" class="step-content hidden"> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <!-- Left Column: Branding Form --> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4">Brand Your Flyer</h2> | |
| <!-- Company Name --> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Company Name</label> | |
| <input type="text" id="company-name" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Your Company Name"> | |
| </div> | |
| <!-- Logo Upload --> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Company Logo</label> | |
| <div class="flex items-center"> | |
| <div id="logo-preview" class="w-16 h-16 bg-gray-100 rounded-md flex items-center justify-center overflow-hidden mr-4"> | |
| <i class="fas fa-image text-gray-400 text-xl"></i> | |
| </div> | |
| <div> | |
| <input type="file" id="logo-upload" class="hidden" accept="image/png,image/jpeg,image/svg+xml"> | |
| <button id="upload-logo-btn" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition"> | |
| Upload Logo | |
| </button> | |
| <button id="remove-logo-btn" class="px-4 py-2 bg-red-100 text-red-600 rounded-md hover:bg-red-200 transition ml-2 hidden"> | |
| Remove | |
| </button> | |
| <p class="text-xs text-gray-500 mt-1">SVG, PNG or JPG (max 1MB)</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Contact Information --> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Contact Information</label> | |
| <div class="space-y-3"> | |
| <div> | |
| <input type="text" id="contact-phone" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Phone Number"> | |
| </div> | |
| <div> | |
| <input type="email" id="contact-email" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Email Address"> | |
| </div> | |
| <div> | |
| <input type="text" id="contact-address" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Physical Address (optional)"> | |
| </div> | |
| <div> | |
| <input type="url" id="contact-website" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Website URL"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Contact Position --> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Contact Position</label> | |
| <div class="flex space-x-4"> | |
| <label class="inline-flex items-center"> | |
| <input type="radio" name="contact-position" value="header" class="h-4 w-4 text-blue-600 focus:ring-blue-500" checked> | |
| <span class="ml-2 text-gray-700">Header</span> | |
| </label> | |
| <label class="inline-flex items-center"> | |
| <input type="radio" name="contact-position" value="footer" class="h-4 w-4 text-blue-600 focus:ring-blue-500"> | |
| <span class="ml-2 text-gray-700">Footer</span> | |
| </label> | |
| <label class="inline-flex items-center"> | |
| <input type="radio" name="contact-position" value="both" class="h-4 w-4 text-blue-600 focus:ring-blue-500"> | |
| <span class="ml-2 text-gray-700">Both</span> | |
| </label> | |
| </div> | |
| </div> | |
| <!-- Color Theme --> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Color Theme</label> | |
| <div class="flex items-center space-x-2 mb-3"> | |
| <div class="color-swatch bg-blue-500 selected" data-color="#3b82f6"></div> | |
| <div class="color-swatch bg-purple-500" data-color="#8b5cf6"></div> | |
| <div class="color-swatch bg-green-500" data-color="#10b981"></div> | |
| <div class="color-swatch bg-red-500" data-color="#ef4444"></div> | |
| <div class="color-swatch bg-yellow-500" data-color="#f59e0b"></div> | |
| <div class="color-swatch bg-pink-500" data-color="#ec4899"></div> | |
| </div> | |
| <div class="flex items-center"> | |
| <span class="mr-2 text-gray-500">#</span> | |
| <input type="text" id="custom-color" class="w-24 px-2 py-1 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="3b82f6" maxlength="6"> | |
| <div id="custom-color-preview" class="ml-2 w-6 h-6 rounded border border-gray-300"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Column: Live Preview --> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4">Live Flyer Preview</h2> | |
| <div id="flyer-preview-container" class="flyer-preview bg-white p-4 rounded-md"> | |
| <!-- Preview will be rendered here --> | |
| <div id="flyer-preview-loading" class="flex flex-col items-center justify-center h-full"> | |
| <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div> | |
| <p class="text-gray-600">Processing your products...</p> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex justify-between"> | |
| <button id="back-step-2" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition"> | |
| <i class="fas fa-arrow-left mr-2"></i> Back | |
| </button> | |
| <div> | |
| <button id="edit-flyer-btn" class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition mr-2"> | |
| <i class="fas fa-edit mr-2"></i> Edit Flyer | |
| </button> | |
| <button id="next-step-2" class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"> | |
| Next <i class="fas fa-arrow-right ml-2"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 3: Final Preview & Export --> | |
| <div id="step-3" class="step-content hidden"> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <h2 class="text-xl font-semibold mb-4">Final Preview & Export</h2> | |
| <div id="final-preview-container" class="flyer-preview bg-white p-4 rounded-md mb-6"> | |
| <!-- Final preview will be rendered here --> | |
| </div> | |
| <div class="flex flex-wrap gap-3 mb-6"> | |
| <button class="export-option px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition flex items-center"> | |
| <i class="fas fa-file-image mr-2"></i> PNG | |
| </button> | |
| <button class="export-option px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition flex items-center"> | |
| <i class="fas fa-file-image mr-2"></i> JPG | |
| </button> | |
| <button class="export-option px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition flex items-center"> | |
| <i class="fas fa-file-pdf mr-2"></i> PDF | |
| </button> | |
| <button class="export-option px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition flex items-center"> | |
| <i class="fas fa-file-code mr-2"></i> HTML | |
| </button> | |
| <button class="export-option px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition flex items-center"> | |
| <i class="fas fa-share-alt mr-2"></i> Share Link | |
| </button> | |
| </div> | |
| <div class="flex justify-between"> | |
| <button id="back-step-3" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition"> | |
| <i class="fas fa-arrow-left mr-2"></i> Back to Editor | |
| </button> | |
| <button class="px-6 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition"> | |
| <i class="fas fa-save mr-2"></i> Save Flyer | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // App state | |
| const state = { | |
| currentStep: 1, | |
| maxProducts: 5, | |
| products: [], | |
| branding: { | |
| companyName: 'Your Company', | |
| logo: null, | |
| contact: { | |
| phone: '', | |
| email: '', | |
| address: '', | |
| website: '' | |
| }, | |
| contactPosition: 'header', | |
| themeColor: '#3b82f6' | |
| }, | |
| flyerHtml: '', | |
| isEditing: false, | |
| currentlyEditing: null | |
| }; | |
| // DOM elements | |
| const stepContents = document.querySelectorAll('.step-content'); | |
| const tabButtons = document.querySelectorAll('.tab-button'); | |
| const urlInputsContainer = document.getElementById('url-inputs'); | |
| const nextStep1Btn = document.getElementById('next-step-1'); | |
| const backStep2Btn = document.getElementById('back-step-2'); | |
| const nextStep2Btn = document.getElementById('next-step-2'); | |
| const backStep3Btn = document.getElementById('back-step-3'); | |
| const productCountEl = document.getElementById('product-count'); | |
| const maxProductsEl = document.getElementById('max-products'); | |
| const logoUploadBtn = document.getElementById('upload-logo-btn'); | |
| const logoUploadInput = document.getElementById('logo-upload'); | |
| const logoPreview = document.getElementById('logo-preview'); | |
| const removeLogoBtn = document.getElementById('remove-logo-btn'); | |
| const flyerPreviewContainer = document.getElementById('flyer-preview-container'); | |
| const flyerPreviewLoading = document.getElementById('flyer-preview-loading'); | |
| const editFlyerBtn = document.getElementById('edit-flyer-btn'); | |
| const finalPreviewContainer = document.getElementById('final-preview-container'); | |
| const exportOptions = document.querySelectorAll('.export-option'); | |
| const colorSwatches = document.querySelectorAll('.color-swatch'); | |
| const customColorInput = document.getElementById('custom-color'); | |
| const customColorPreview = document.getElementById('custom-color-preview'); | |
| const companyNameInput = document.getElementById('company-name'); | |
| // Initialize | |
| maxProductsEl.textContent = state.maxProducts; | |
| createUrlInputs(); | |
| setupEventListeners(); | |
| // Functions | |
| function setupEventListeners() { | |
| // Navigation | |
| tabButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| const step = parseInt(button.dataset.step); | |
| goToStep(step); | |
| }); | |
| }); | |
| nextStep1Btn.addEventListener('click', () => { | |
| collectProductUrls(); | |
| goToStep(2); | |
| simulateScraping(); | |
| }); | |
| backStep2Btn.addEventListener('click', () => goToStep(1)); | |
| nextStep2Btn.addEventListener('click', () => { | |
| if (confirm('Proceed to final preview and export?')) { | |
| goToStep(3); | |
| } | |
| }); | |
| backStep3Btn.addEventListener('click', () => goToStep(2)); | |
| // Logo upload | |
| logoUploadBtn.addEventListener('click', () => logoUploadInput.click()); | |
| logoUploadInput.addEventListener('change', handleLogoUpload); | |
| removeLogoBtn.addEventListener('click', removeLogo); | |
| // Branding inputs | |
| companyNameInput.addEventListener('input', updateBranding); | |
| document.getElementById('contact-phone').addEventListener('input', updateBranding); | |
| document.getElementById('contact-email').addEventListener('input', updateBranding); | |
| document.getElementById('contact-address').addEventListener('input', updateBranding); | |
| document.getElementById('contact-website').addEventListener('input', updateBranding); | |
| document.querySelectorAll('input[name="contact-position"]').forEach(radio => { | |
| radio.addEventListener('change', updateBranding); | |
| }); | |
| // Color theme | |
| colorSwatches.forEach(swatch => { | |
| swatch.addEventListener('click', () => { | |
| colorSwatches.forEach(s => s.classList.remove('selected')); | |
| swatch.classList.add('selected'); | |
| const color = swatch.dataset.color; | |
| state.branding.themeColor = color; | |
| customColorInput.value = color.substring(1); | |
| customColorPreview.style.backgroundColor = color; | |
| updateBranding(); | |
| }); | |
| }); | |
| customColorInput.addEventListener('input', () => { | |
| const color = '#' + customColorInput.value; | |
| if (/^#[0-9A-F]{6}$/i.test(color)) { | |
| state.branding.themeColor = color; | |
| customColorPreview.style.backgroundColor = color; | |
| colorSwatches.forEach(s => s.classList.remove('selected')); | |
| updateBranding(); | |
| } | |
| }); | |
| // Export options | |
| exportOptions.forEach(option => { | |
| option.addEventListener('click', () => { | |
| const format = option.textContent.trim().toLowerCase(); | |
| alert(`Exporting as ${format}...`); | |
| }); | |
| }); | |
| // Edit flyer button | |
| editFlyerBtn.addEventListener('click', toggleEditMode); | |
| } | |
| function createUrlInputs() { | |
| urlInputsContainer.innerHTML = ''; | |
| for (let i = 0; i < state.maxProducts; i++) { | |
| const div = document.createElement('div'); | |
| div.className = 'flex items-center'; | |
| div.innerHTML = ` | |
| <div class="product-placeholder w-16 h-16 mr-4"> | |
| <i class="fas fa-image text-gray-400 text-xl"></i> | |
| </div> | |
| <div class="flex-1"> | |
| <input type="url" class="product-url w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://example.com/product"> | |
| </div> | |
| `; | |
| urlInputsContainer.appendChild(div); | |
| } | |
| // Add event listeners to URL inputs | |
| document.querySelectorAll('.product-url').forEach(input => { | |
| input.addEventListener('input', validateStep1); | |
| }); | |
| } | |
| function validateStep1() { | |
| const urls = Array.from(document.querySelectorAll('.product-url')) | |
| .map(input => input.value.trim()) | |
| .filter(url => url !== ''); | |
| state.products = urls.map(url => ({ url, status: 'pending' })); | |
| productCountEl.textContent = urls.length; | |
| nextStep1Btn.disabled = urls.length === 0; | |
| } | |
| function collectProductUrls() { | |
| state.products = Array.from(document.querySelectorAll('.product-url')) | |
| .map(input => ({ | |
| url: input.value.trim(), | |
| status: 'pending', | |
| productName: '', | |
| productNumber: `SKU-${Math.floor(Math.random() * 10000)}`, | |
| description: '', | |
| price: '', | |
| heroImage: '', | |
| availableColors: ['Red', 'Blue', 'Green'].slice(0, Math.floor(Math.random() * 3) + 1), | |
| pricingTableHTML: Math.random() > 0.5 ? '<table><tr><th>Qty</th><th>Price</th></tr><tr><td>1-9</td><td>$10.99</td></tr><tr><td>10+</td><td>$8.99</td></tr></table>' : null | |
| })) | |
| .filter(product => product.url !== ''); | |
| } | |
| function simulateScraping() { | |
| flyerPreviewLoading.classList.remove('hidden'); | |
| flyerPreviewContainer.innerHTML = ''; | |
| flyerPreviewContainer.appendChild(flyerPreviewLoading); | |
| // Simulate scraping delay | |
| setTimeout(() => { | |
| // Generate mock product data | |
| state.products = state.products.map((product, index) => ({ | |
| ...product, | |
| status: 'success', | |
| productName: `Premium Product ${index + 1}`, | |
| description: `This is a detailed description for Premium Product ${index + 1}. It features high-quality materials and excellent craftsmanship that customers love.`, | |
| price: { | |
| min: Math.random() * 100 + 10, | |
| currency: 'USD', | |
| minimumQuantity: Math.random() > 0.7 ? Math.floor(Math.random() * 10) + 1 : undefined | |
| }, | |
| heroImage: `https://picsum.photos/400/300?random=${index}`, | |
| benefits: [ | |
| `Saves time with efficient design`, | |
| `Reduces costs with durable materials`, | |
| `Improves productivity by ${Math.floor(Math.random() * 50) + 10}%` | |
| ] | |
| })); | |
| renderFlyerPreview(); | |
| }, 2000); | |
| } | |
| function renderFlyerPreview() { | |
| flyerPreviewLoading.classList.add('hidden'); | |
| const { companyName, logo, contact, contactPosition, themeColor } = state.branding; | |
| let html = ` | |
| <div class="flyer-preview-content" style="--theme-color: ${themeColor}"> | |
| ${contactPosition === 'header' || contactPosition === 'both' ? renderContactHeader() : ''} | |
| <div class="p-6"> | |
| <h1 class="text-3xl font-bold mb-6 text-center" style="color: ${themeColor}">Our Featured Products</h1> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| `; | |
| state.products.forEach((product, index) => { | |
| const priceStr = typeof product.price === 'object' | |
| ? `$${product.price.min.toFixed(2)}${product.price.minimumQuantity ? ` (Min. ${product.price.minimumQuantity})` : ''}` | |
| : product.price || 'Price not available'; | |
| html += ` | |
| <div class="product-card relative border rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-md transition-shadow duration-300"> | |
| ${renderStatusBadge(product.status)} | |
| ${state.isEditing ? ` | |
| <div class="editor-toolbar"> | |
| <button class="p-1 text-blue-500 hover:text-blue-700" title="Edit Text" data-action="edit-text" data-product="${index}"> | |
| <i class="fas fa-font text-xs"></i> | |
| </button> | |
| <button class="p-1 text-blue-500 hover:text-blue-700" title="Replace Image" data-action="replace-image" data-product="${index}"> | |
| <i class="fas fa-image text-xs"></i> | |
| </button> | |
| <button class="p-1 text-blue-500 hover:text-blue-700" title="Change Layout" data-action="change-layout" data-product="${index}"> | |
| <i class="fas fa-th text-xs"></i> | |
| </button> | |
| </div> | |
| ` : ''} | |
| <div class="h-48 bg-gray-100 flex items-center justify-center overflow-hidden relative"> | |
| ${product.heroImage ? | |
| `<img src="${product.heroImage}" alt="${product.productName}" class="w-full h-full object-cover editable-image" data-product="${index}">` : | |
| `<i class="fas fa-image text-gray-400 text-4xl"></i>`} | |
| </div> | |
| <div class="p-4"> | |
| <div class="flex justify-between items-start"> | |
| <h3 class="text-xl font-semibold mb-2 editable" data-field="productName" data-product="${index}">${product.productName}</h3> | |
| <span class="text-sm bg-gray-100 px-2 py-1 rounded editable" data-field="productNumber" data-product="${index}">${product.productNumber}</span> | |
| </div> | |
| <p class="text-gray-600 mb-3 editable" data-field="description" data-product="${index}">${product.description}</p> | |
| ${product.benefits && product.benefits.length > 0 ? ` | |
| <div class="mb-3"> | |
| <h4 class="text-sm font-medium mb-1" style="color: ${themeColor}">Key Benefits:</h4> | |
| <ul class="text-sm space-y-1 editable" data-field="benefits" data-product="${index}"> | |
| ${product.benefits.map(benefit => `<li class="flex items-start"> | |
| <i class="fas fa-check-circle text-green-500 mt-1 mr-2 text-xs"></i> | |
| <span>${benefit}</span> | |
| </li>`).join('')} | |
| </ul> | |
| </div> | |
| ` : ''} | |
| <div class="font-bold text-lg mb-2 editable" data-field="price" data-product="${index}" style="color: ${themeColor}">${priceStr}</div> | |
| ${product.availableColors && product.availableColors.length > 0 ? ` | |
| <div class="flex items-center text-sm text-gray-600 editable" data-field="colors" data-product="${index}"> | |
| <span class="mr-2">Colors:</span> | |
| <div class="flex space-x-1"> | |
| ${product.availableColors.map(color => ` | |
| <span class="inline-block w-4 h-4 rounded-full border border-gray-300" style="background-color: ${color.toLowerCase()}"></span> | |
| `).join('')} | |
| </div> | |
| </div> | |
| ` : ''} | |
| ${product.pricingTableHTML ? ` | |
| <div class="mt-3 text-xs"> | |
| <details> | |
| <summary class="text-blue-500 cursor-pointer">View quantity pricing</summary> | |
| <div class="mt-2 overflow-x-auto"> | |
| ${product.pricingTableHTML} | |
| </div> | |
| </details> | |
| </div> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| html += ` | |
| </div> | |
| </div> | |
| ${contactPosition === 'footer' || contactPosition === 'both' ? renderContactFooter() : ''} | |
| </div> | |
| `; | |
| flyerPreviewContainer.innerHTML = html; | |
| if (state.isEditing) { | |
| setupEditor(); | |
| } | |
| } | |
| function renderStatusBadge(status) { | |
| const statusMap = { | |
| 'pending': { text: 'Pending', color: 'bg-gray-500' }, | |
| 'scraping': { text: 'Scraping...', color: 'bg-blue-500' }, | |
| 'success': { text: 'Ready', color: 'bg-green-500' }, | |
| 'error': { text: 'Error', color: 'bg-red-500' }, | |
| 'image_upload_required': { text: 'Needs Image', color: 'bg-yellow-500' } | |
| }; | |
| const statusInfo = statusMap[status] || statusMap.pending; | |
| return ` | |
| <div class="status-badge px-2 py-1 text-xs text-white rounded ${statusInfo.color}"> | |
| ${statusInfo.text} | |
| </div> | |
| `; | |
| } | |
| function renderContactHeader() { | |
| const { companyName, logo, contact } = state.branding; | |
| return ` | |
| <div class="flyer-header p-4 flex justify-between items-center"> | |
| <div class="flex items-center"> | |
| ${logo ? | |
| `<img src="${logo}" alt="Company Logo" class="h-12 editable-image" data-field="logo">` : | |
| `<div class="h-12 w-32 bg-white rounded flex items-center justify-center editable" data-field="logo-placeholder"> | |
| <span class="font-bold" style="color: var(--theme-color)">Your Logo</span> | |
| </div>`} | |
| <div class="company-name ml-4 editable" data-field="companyName">${companyName}</div> | |
| </div> | |
| <div class="text-right text-sm"> | |
| ${contact.phone ? `<div class="editable" data-field="phone"><i class="fas fa-phone mr-1"></i> ${contact.phone}</div>` : | |
| `<div class="editable" data-field="phone-placeholder"><i class="fas fa-phone mr-1"></i> Phone</div>`} | |
| ${contact.email ? `<div class="editable" data-field="email"><i class="fas fa-envelope mr-1"></i> ${contact.email}</div>` : | |
| `<div class="editable" data-field="email-placeholder"><i class="fas fa-envelope mr-1"></i> Email</div>`} | |
| ${contact.website ? `<div class="editable" data-field="website"><i class="fas fa-globe mr-1"></i> ${contact.website}</div>` : | |
| `<div class="editable" data-field="website-placeholder"><i class="fas fa-globe mr-1"></i> Website</div>`} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function renderContactFooter() { | |
| const { companyName, contact } = state.branding; | |
| return ` | |
| <div class="flyer-footer p-4"> | |
| <div class="flex justify-between items-center"> | |
| <div class="company-name editable" data-field="companyName">${companyName}</div> | |
| <div class="text-sm"> | |
| ${contact.phone || contact.email || contact.website || contact.address ? ` | |
| <div class="flex flex-wrap justify-center gap-4"> | |
| ${contact.phone ? `<div class="editable" data-field="phone"><i class="fas fa-phone mr-1"></i> ${contact.phone}</div>` : ''} | |
| ${contact.email ? `<div class="editable" data-field="email"><i class="fas fa-envelope mr-1"></i> ${contact.email}</div>` : ''} | |
| ${contact.website ? `<div class="editable" data-field="website"><i class="fas fa-globe mr-1"></i> ${contact.website}</div>` : ''} | |
| ${contact.address ? `<div class="editable" data-field="address"><i class="fas fa-map-marker-alt mr-1"></i> ${contact.address}</div>` : ''} | |
| </div> | |
| ` : ` | |
| <div class="editable" data-field="contact-placeholder">Contact Information</div> | |
| `} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function renderFinalPreview() { | |
| finalPreviewContainer.innerHTML = flyerPreviewContainer.innerHTML; | |
| } | |
| function handleLogoUpload(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| // Check file size (1MB max) | |
| if (file.size > 1024 * 1024) { | |
| alert('Logo must be 1MB or smaller'); | |
| return; | |
| } | |
| // Check file type | |
| const validTypes = ['image/jpeg', 'image/png', 'image/svg+xml']; | |
| if (!validTypes.includes(file.type)) { | |
| alert('Only JPG, PNG, or SVG files are allowed'); | |
| return; | |
| } | |
| // Create preview | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| logoPreview.innerHTML = `<img src="${e.target.result}" alt="Logo Preview" class="w-full h-full object-contain">`; | |
| removeLogoBtn.classList.remove('hidden'); | |
| state.branding.logo = e.target.result; | |
| updateBranding(); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| function removeLogo() { | |
| logoPreview.innerHTML = '<i class="fas fa-image text-gray-400 text-xl"></i>'; | |
| removeLogoBtn.classList.add('hidden'); | |
| logoUploadInput.value = ''; | |
| state.branding.logo = null; | |
| updateBranding(); | |
| } | |
| function updateBranding() { | |
| state.branding.companyName = companyNameInput.value || 'Your Company'; | |
| state.branding.contact = { | |
| phone: document.getElementById('contact-phone').value, | |
| email: document.getElementById('contact-email').value, | |
| address: document.getElementById('contact-address').value, | |
| website: document.getElementById('contact-website').value | |
| }; | |
| state.branding.contactPosition = document.querySelector('input[name="contact-position"]:checked').value; | |
| if (state.currentStep === 2 && state.products.length > 0) { | |
| renderFlyerPreview(); | |
| } | |
| } | |
| function toggleEditMode() { | |
| state.isEditing = !state.isEditing; | |
| editFlyerBtn.innerHTML = state.isEditing | |
| ? '<i class="fas fa-check mr-2"></i> Finish Editing' | |
| : '<i class="fas fa-edit mr-2"></i> Edit Flyer'; | |
| renderFlyerPreview(); | |
| } | |
| function setupEditor() { | |
| // Make text editable | |
| document.querySelectorAll('.editable').forEach(el => { | |
| el.addEventListener('click', handleEditableClick); | |
| }); | |
| // Make images editable | |
| document.querySelectorAll('.editable-image').forEach(el => { | |
| el.addEventListener('click', handleImageEditClick); | |
| }); | |
| // Setup editor toolbar buttons | |
| document.querySelectorAll('[data-action]').forEach(button => { | |
| button.addEventListener('click', handleEditorAction); | |
| }); | |
| } | |
| function handleEditableClick(e) { | |
| if (!state.isEditing) return; | |
| const element = e.currentTarget; | |
| const field = element.dataset.field; | |
| const productIndex = element.dataset.product; | |
| // Store the current editing element | |
| state.currentlyEditing = { element, field, productIndex }; | |
| // Create an input field | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.className = 'w-full px-2 py-1 border border-blue-500 rounded'; | |
| input.value = element.textContent.trim(); | |
| // Replace the element with input | |
| element.replaceWith(input); | |
| input.focus(); | |
| // Handle input completion | |
| input.addEventListener('blur', () => completeEditing(input)); | |
| input.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') completeEditing(input); | |
| }); | |
| } | |
| function handleImageEditClick(e) { | |
| if (!state.isEditing) return; | |
| const element = e.currentTarget; | |
| const field = element.dataset.field; | |
| const productIndex = element.dataset.product; | |
| // Create file input | |
| const fileInput = document.createElement('input'); | |
| fileInput.type = 'file'; | |
| fileInput.accept = 'image/*'; | |
| fileInput.className = 'hidden'; | |
| // Add to DOM and trigger click | |
| document.body.appendChild(fileInput); | |
| fileInput.click(); | |
| // Handle file selection | |
| fileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| if (productIndex !== undefined) { | |
| // Update product image | |
| state.products[productIndex].heroImage = e.target.result; | |
| } else if (field === 'logo') { | |
| // Update logo | |
| state.branding.logo = e.target.result; | |
| logoPreview.innerHTML = `<img src="${e.target.result}" alt="Logo Preview" class="w-full h-full object-contain">`; | |
| removeLogoBtn.classList.remove('hidden'); | |
| } | |
| renderFlyerPreview(); | |
| }; | |
| reader.readAsDataURL(file); | |
| // Clean up | |
| document.body.removeChild(fileInput); | |
| }); | |
| } | |
| function handleEditorAction(e) { | |
| const action = e.currentTarget.dataset.action; | |
| const productIndex = e.currentTarget.dataset.product; | |
| switch (action) { | |
| case 'edit-text': | |
| alert(`Editing text for product ${parseInt(productIndex) + 1}`); | |
| break; | |
| case 'replace-image': | |
| handleImageEditClick({ currentTarget: document.querySelector(`.editable-image[data-product="${productIndex}"]`) }); | |
| break; | |
| case 'change-layout': | |
| alert(`Changing layout for product ${parseInt(productIndex) + 1}`); | |
| break; | |
| } | |
| } | |
| function completeEditing(input) { | |
| if (!state.currentlyEditing) return; | |
| const { element, field, productIndex } = state.currentlyEditing; | |
| const newValue = input.value.trim(); | |
| if (productIndex !== undefined) { | |
| // Update product field | |
| if (field === 'productName') { | |
| state.products[productIndex].productName = newValue; | |
| } else if (field === 'productNumber') { | |
| state.products[productIndex].productNumber = newValue; | |
| } else if (field === 'description') { | |
| state.products[productIndex].description = newValue; | |
| } | |
| } else { | |
| // Update branding field | |
| if (field === 'companyName') { | |
| state.branding.companyName = newValue; | |
| companyNameInput.value = newValue; | |
| } else if (field === 'phone') { | |
| state.branding.contact.phone = newValue; | |
| document.getElementById('contact-phone').value = newValue; | |
| } else if (field === 'email') { | |
| state.branding.contact.email = newValue; | |
| document.getElementById('contact-email').value = newValue; | |
| } else if (field === 'website') { | |
| state.branding.contact.website = newValue; | |
| document.getElementById('contact-website').value = newValue; | |
| } else if (field === 'address') { | |
| state.branding.contact.address = newValue; | |
| document.getElementById('contact-address').value = newValue; | |
| } | |
| } | |
| // Re-render to update all instances | |
| renderFlyerPreview(); | |
| // Clean up | |
| document.body.removeChild(input); | |
| state.currentlyEditing = null; | |
| } | |
| function goToStep(step) { | |
| if (step < 1 || step > 3) return; | |
| // Update UI | |
| stepContents.forEach(content => content.classList.add('hidden')); | |
| document.getElementById(`step-${step}`).classList.remove('hidden'); | |
| tabButtons.forEach(button => { | |
| button.classList.remove('active'); | |
| const buttonStep = parseInt(button.dataset.step); | |
| if (buttonStep === step) { | |
| button.classList.add('active'); | |
| } else { | |
| const numberSpan = button.querySelector('span'); | |
| if (buttonStep < step) { | |
| numberSpan.className = 'flex items-center justify-center w-8 h-8 bg-green-500 text-white rounded-full mr-2'; | |
| numberSpan.innerHTML = '<i class="fas fa-check text-xs"></i>'; | |
| } else { | |
| numberSpan.className = 'flex items-center justify-center w-8 h-8 bg-gray-200 text-gray-600 rounded-full mr-2'; | |
| numberSpan.textContent = buttonStep; | |
| } | |
| } | |
| }); | |
| state.currentStep = step; | |
| // Special handling for step 3 | |
| if (step === 3) { | |
| renderFinalPreview(); | |
| } | |
| } | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=SpectrumInfinite/link2flyer-si" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |