965 lines
50 KiB
HTML
Executable file
965 lines
50 KiB
HTML
Executable file
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Amazon BTF Composer</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f8f9fa; color: #333; line-height: 1.6; }
|
|
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
|
|
.header { text-align: center; margin-bottom: 30px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.header h1 { color: #232f3e; margin-bottom: 10px; }
|
|
.header p { color: #666; font-size: 16px; }
|
|
|
|
.upload-section { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px; }
|
|
.upload-box { background: white; border: 2px dashed #ddd; border-radius: 8px; padding: 30px; text-align: center; transition: all 0.3s ease; cursor: pointer; }
|
|
.upload-box:hover { border-color: #ff9900; background: #fff8f0; }
|
|
.upload-box.dragover { border-color: #ff9900; background: #fff8f0; transform: scale(1.02); }
|
|
.upload-icon { font-size: 48px; margin-bottom: 15px; color: #666; }
|
|
.upload-text { font-size: 18px; font-weight: 600; margin-bottom: 10px; color: #333; }
|
|
.upload-subtext { color: #666; font-size: 14px; }
|
|
.file-input { display: none; }
|
|
|
|
.preview-section { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; margin-bottom: 30px; }
|
|
.image-preview, .csv-preview { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 10px; }
|
|
.image-thumb { width: 80px; height: 80px; object-fit: cover; border-radius: 4px; border: 2px solid #ddd; cursor: pointer; transition: all 0.3s ease; }
|
|
.image-thumb:hover { border-color: #ff9900; transform: scale(1.05); }
|
|
.csv-data { background: #f8f9fa; border-radius: 4px; padding: 15px; font-family: monospace; font-size: 14px; max-height: 200px; overflow-y: auto; }
|
|
|
|
.composer-section { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; margin-bottom: 30px; }
|
|
.module-library { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.module-library h3 { margin-bottom: 15px; color: #232f3e; }
|
|
.module-btn { width: 100%; background: #ff9900; color: white; border: none; padding: 12px; border-radius: 4px; cursor: pointer; font-weight: 600; margin-bottom: 10px; transition: background 0.3s ease; }
|
|
.module-btn:hover { background: #e88900; }
|
|
|
|
.btf-stack { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.btf-stack h3 { margin-bottom: 15px; color: #232f3e; }
|
|
.stack-empty { text-align: center; color: #999; padding: 40px; }
|
|
|
|
.module-card { background: #f8f9fa; border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin-bottom: 15px; position: relative; }
|
|
.module-card.dragging { opacity: 0.5; }
|
|
.module-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
|
.drag-handle { cursor: grab; color: #999; font-size: 20px; }
|
|
.drag-handle:active { cursor: grabbing; }
|
|
.module-title { flex: 1; font-weight: 600; color: #232f3e; }
|
|
.module-controls { display: flex; gap: 5px; }
|
|
.control-btn { background: #666; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
|
.control-btn:hover { background: #555; }
|
|
.control-btn.remove { background: #dc3545; }
|
|
.control-btn.remove:hover { background: #c82333; }
|
|
|
|
.module-editor { display: none; margin-top: 10px; padding: 10px; background: white; border-radius: 4px; }
|
|
.module-editor.visible { display: block; }
|
|
.editor-field { margin-bottom: 10px; }
|
|
.editor-field label { display: block; font-size: 13px; color: #666; margin-bottom: 5px; }
|
|
.editor-field input, .editor-field textarea, .editor-field select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
|
|
.editor-field textarea { min-height: 80px; resize: vertical; }
|
|
|
|
.comparison-row { display: grid; grid-template-columns: 200px repeat(3, 1fr); gap: 8px; margin-bottom: 8px; }
|
|
.comparison-row input { padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
|
|
.faq-pair { margin-bottom: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px; }
|
|
.feature-item-edit { margin-bottom: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px; display: flex; gap: 8px; }
|
|
|
|
.add-btn { background: #28a745; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; margin-top: 10px; }
|
|
.add-btn:hover { background: #218838; }
|
|
|
|
.toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 20px; }
|
|
.export-btn { background: #ff9900; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 600; }
|
|
.export-btn:hover { background: #e88900; }
|
|
|
|
.mockup-container { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
|
.amazon-mockup { background: white; border: 1px solid #ddd; border-radius: 4px; padding: 20px; font-family: Arial, sans-serif; transition: all 0.3s ease; }
|
|
.amazon-mockup.mobile { max-width: 375px; margin: 0 auto; }
|
|
.btf-module { margin-bottom: 30px; padding-bottom: 30px; border-bottom: 2px solid #ddd; }
|
|
.btf-module:last-child { border-bottom: none; }
|
|
.btf-module h4 { color: #232f3e; margin-bottom: 15px; }
|
|
|
|
.brand-story-hero { width: 100%; aspect-ratio: 1464/600; background: #f0f0f0; border-radius: 8px; margin-bottom: 15px; display: flex; align-items: center; justify-content: center; color: #666; overflow: hidden; }
|
|
.brand-story-hero img { width: 100%; height: 100%; object-fit: contain; border-radius: 8px; }
|
|
.brand-story-text { font-size: 15px; line-height: 1.6; }
|
|
|
|
.comparison-table { width: 100%; border-collapse: collapse; }
|
|
.comparison-table th, .comparison-table td { border: 1px solid #ddd; padding: 10px; text-align: left; font-size: 14px; }
|
|
.comparison-table th { background: #f8f9fa; font-weight: 600; }
|
|
|
|
.faq-item { margin-bottom: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px; }
|
|
.faq-question { font-weight: 600; font-size: 15px; margin-bottom: 8px; color: #232f3e; }
|
|
.faq-answer { font-size: 14px; line-height: 1.5; }
|
|
|
|
.video-placeholder { width: 100%; height: 300px; background: #f0f0f0; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #666; }
|
|
.video-placeholder img { width: 100%; height: 100%; object-fit: cover; border-radius: 8px; }
|
|
|
|
.features-section { }
|
|
.feature-item { margin-bottom: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px; display: flex; gap: 12px; }
|
|
.feature-icon { width: 50px; height: 50px; background: #e0e0e0; border-radius: 8px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 24px; }
|
|
.feature-content { flex: 1; }
|
|
.feature-headline { font-weight: 600; font-size: 15px; margin-bottom: 6px; color: #232f3e; }
|
|
.feature-text { font-size: 14px; line-height: 1.4; }
|
|
.upload-video-btn { background: #28a745; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; margin-top: 5px; }
|
|
.upload-video-btn:hover { background: #218838; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Amazon BTF Composer</h1>
|
|
<p>Build your Below The Fold content modules</p>
|
|
</div>
|
|
|
|
<!-- Upload Section -->
|
|
<div class="upload-section">
|
|
<div class="upload-box" id="imageUpload">
|
|
<div class="upload-icon">📸</div>
|
|
<div class="upload-text">Drop Product Images Here</div>
|
|
<div class="upload-subtext">or click to browse files</div>
|
|
<input type="file" class="file-input" id="imageInput" multiple accept="image/jpeg,image/jpg,image/png">
|
|
</div>
|
|
<div class="upload-box" id="csvUpload">
|
|
<div class="upload-icon">📊</div>
|
|
<div class="upload-text">Upload CSV Data</div>
|
|
<div class="upload-subtext">or click to browse file</div>
|
|
<input type="file" class="file-input" id="csvInput" accept=".csv">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Section -->
|
|
<div class="preview-section">
|
|
<div class="image-preview">
|
|
<h3>Uploaded Images</h3>
|
|
<div class="image-grid" id="imageGrid"><div style="color:#999;">No images uploaded yet</div></div>
|
|
</div>
|
|
<div class="csv-preview">
|
|
<h3>CSV Data Preview</h3>
|
|
<div class="csv-data" id="csvData"><div style="color:#999;">No CSV data loaded yet</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Composer Section -->
|
|
<div class="composer-section">
|
|
<div class="module-library">
|
|
<h3>Module Library</h3>
|
|
<button class="module-btn" onclick="composer.addModule('brandStory')">+ Brand Story</button>
|
|
<button class="module-btn" onclick="composer.addModule('comparison')">+ Comparison Table</button>
|
|
<button class="module-btn" onclick="composer.addModule('faq')">+ FAQ</button>
|
|
<button class="module-btn" onclick="composer.addModule('video')">+ Video</button>
|
|
<button class="module-btn" onclick="composer.addModule('features')">+ Feature Callouts</button>
|
|
</div>
|
|
|
|
<div class="btf-stack">
|
|
<h3>BTF Module Stack</h3>
|
|
<div id="moduleStack">
|
|
<div class="stack-empty">No modules added yet. Click a module from the library to add it.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toolbar -->
|
|
<div class="toolbar">
|
|
<label style="display:flex; align-items:center; gap:6px;"><span>EAN</span><input id="eanInput" type="text" style="padding:8px; border:1px solid #ddd; border-radius:4px; width:180px;" placeholder="13-digit EAN"></label>
|
|
<label style="display:flex; align-items:center; gap:6px;"><span>Preview</span><select id="viewportToggle" onchange="composer.toggleViewport(this.value)" style="padding:8px; border:1px solid #ddd; border-radius:4px;"><option value="desktop">Desktop</option><option value="mobile">Mobile</option></select></label>
|
|
<button class="export-btn" onclick="composer.autoPopulateFromCSV()">Create modules from CSV</button>
|
|
<button class="export-btn" onclick="composer.exportCSV()">Export Content as CSV</button>
|
|
<button class="export-btn" onclick="composer.exportJSON()">Export JSON</button>
|
|
<button class="export-btn" onclick="composer.importJSON()">Import JSON</button>
|
|
<button class="export-btn" onclick="composer.exportBTF()">Export BTF as JPG</button>
|
|
<button class="export-btn" style="background:#dc3545;" onclick="composer.clearAll()">Clear All</button>
|
|
</div>
|
|
|
|
<!-- Mockup Container -->
|
|
<div class="mockup-container">
|
|
<h3 style="margin-bottom:20px;">BTF Preview</h3>
|
|
<div class="amazon-mockup" id="btfMockup">
|
|
<div style="text-align:center; color:#999; padding:40px;">No modules added yet</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
<script>
|
|
class BTFComposer {
|
|
constructor() {
|
|
this.modules = [];
|
|
this.uploadedImages = [];
|
|
this.csvData = {};
|
|
this.ean = '';
|
|
this.draggedIndex = null;
|
|
this.initializeEventListeners();
|
|
this.loadFromLocalStorage();
|
|
}
|
|
|
|
initializeEventListeners() {
|
|
const imageUpload = document.getElementById('imageUpload');
|
|
const imageInput = document.getElementById('imageInput');
|
|
imageUpload.addEventListener('click', () => imageInput.click());
|
|
imageUpload.addEventListener('dragover', (e) => { e.preventDefault(); e.currentTarget.classList.add('dragover'); });
|
|
imageUpload.addEventListener('dragleave', (e) => { e.preventDefault(); e.currentTarget.classList.remove('dragover'); });
|
|
imageUpload.addEventListener('drop', (e) => { e.preventDefault(); e.currentTarget.classList.remove('dragover'); const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/')); this.processImages(files); });
|
|
imageInput.addEventListener('change', (e) => this.processImages(Array.from(e.target.files)));
|
|
|
|
const csvUpload = document.getElementById('csvUpload');
|
|
const csvInput = document.getElementById('csvInput');
|
|
csvUpload.addEventListener('click', () => csvInput.click());
|
|
csvUpload.addEventListener('dragover', (e) => { e.preventDefault(); e.currentTarget.classList.add('dragover'); });
|
|
csvUpload.addEventListener('dragleave', (e) => { e.preventDefault(); e.currentTarget.classList.remove('dragover'); });
|
|
csvUpload.addEventListener('drop', (e) => { e.preventDefault(); e.currentTarget.classList.remove('dragover'); const files = Array.from(e.dataTransfer.files).filter(f => f.name.endsWith('.csv')); if (files[0]) this.processCsv(files[0]); });
|
|
csvInput.addEventListener('change', (e) => { if (e.target.files[0]) this.processCsv(e.target.files[0]); });
|
|
|
|
document.getElementById('eanInput').addEventListener('change', (e) => { this.ean = e.target.value; this.saveToLocalStorage(); });
|
|
}
|
|
|
|
processImages(files) {
|
|
files.forEach(file => {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
this.uploadedImages.push({ name: file.name, data: e.target.result });
|
|
this.updateImagePreview();
|
|
this.renderModules();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
processCsv(file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
this.csvData = this.parseCsv(e.target.result);
|
|
this.updateCsvPreview();
|
|
this.autoFillExistingModules();
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
parseCsv(text) {
|
|
const lines = text.split(/\r?\n/);
|
|
const data = {};
|
|
lines.forEach(line => {
|
|
const [field, , value] = line.split(',');
|
|
if (field && value) data[field.trim()] = value.trim().replace(/^"|"$/g, '');
|
|
});
|
|
return data;
|
|
}
|
|
|
|
updateImagePreview() {
|
|
const grid = document.getElementById('imageGrid');
|
|
if (!this.uploadedImages.length) {
|
|
grid.innerHTML = '<div style="color:#999;">No images uploaded yet</div>';
|
|
return;
|
|
}
|
|
grid.innerHTML = '';
|
|
this.uploadedImages.forEach(img => {
|
|
const el = document.createElement('img');
|
|
el.src = img.data;
|
|
el.className = 'image-thumb';
|
|
el.title = img.name;
|
|
grid.appendChild(el);
|
|
});
|
|
}
|
|
|
|
updateCsvPreview() {
|
|
const box = document.getElementById('csvData');
|
|
if (!Object.keys(this.csvData).length) {
|
|
box.innerHTML = '<div style="color:#999;">No CSV data loaded yet</div>';
|
|
return;
|
|
}
|
|
let html = '';
|
|
Object.entries(this.csvData).forEach(([k, v]) => {
|
|
html += `<div><strong>${k}:</strong> ${v}</div>`;
|
|
});
|
|
box.innerHTML = html;
|
|
}
|
|
|
|
addModule(type) {
|
|
const module = {
|
|
id: Date.now(),
|
|
type: type,
|
|
data: this.getDefaultModuleData(type)
|
|
};
|
|
this.modules.push(module);
|
|
this.renderStack();
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
getDefaultModuleData(type) {
|
|
switch(type) {
|
|
case 'brandStory': return { text: '', heroIndex: -1 };
|
|
case 'comparison': return { rows: [{ attr: '', vals: ['', '', ''] }] };
|
|
case 'faq': return { pairs: [{ q: '', a: '' }] };
|
|
case 'video': return { url: '', videoData: null, thumbIndex: -1 };
|
|
case 'features': return { items: [{ icon: '⭐', headline: '', text: '' }] };
|
|
default: return {};
|
|
}
|
|
}
|
|
|
|
renderStack() {
|
|
const stack = document.getElementById('moduleStack');
|
|
if (!this.modules.length) {
|
|
stack.innerHTML = '<div class="stack-empty">No modules added yet. Click a module from the library to add it.</div>';
|
|
return;
|
|
}
|
|
|
|
stack.innerHTML = '';
|
|
this.modules.forEach((module, index) => {
|
|
const card = document.createElement('div');
|
|
card.className = 'module-card';
|
|
card.draggable = true;
|
|
card.dataset.index = index;
|
|
|
|
card.addEventListener('dragstart', (e) => {
|
|
this.draggedIndex = index;
|
|
card.classList.add('dragging');
|
|
});
|
|
card.addEventListener('dragend', (e) => {
|
|
card.classList.remove('dragging');
|
|
this.draggedIndex = null;
|
|
});
|
|
card.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
card.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
|
const [moved] = this.modules.splice(this.draggedIndex, 1);
|
|
this.modules.splice(index, 0, moved);
|
|
this.renderStack();
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
});
|
|
|
|
card.innerHTML = `
|
|
<div class="module-header">
|
|
<span class="drag-handle">☰</span>
|
|
<span class="module-title">${this.getModuleTitle(module.type)}</span>
|
|
<div class="module-controls">
|
|
<button class="control-btn" onclick="composer.moveUp(${index})">↑</button>
|
|
<button class="control-btn" onclick="composer.moveDown(${index})">↓</button>
|
|
<button class="control-btn" onclick="composer.toggleEditor(${index})">Edit</button>
|
|
<button class="control-btn remove" onclick="composer.removeModule(${index})">Remove</button>
|
|
</div>
|
|
</div>
|
|
<div class="module-editor" id="editor-${module.id}">
|
|
${this.getEditorHTML(module, index)}
|
|
</div>
|
|
`;
|
|
stack.appendChild(card);
|
|
});
|
|
}
|
|
|
|
getModuleTitle(type) {
|
|
const titles = {
|
|
brandStory: 'Brand Story',
|
|
comparison: 'Comparison Table',
|
|
faq: 'FAQ',
|
|
video: 'Video',
|
|
features: 'Feature Callouts'
|
|
};
|
|
return titles[type] || type;
|
|
}
|
|
|
|
getEditorHTML(module, index) {
|
|
switch(module.type) {
|
|
case 'brandStory':
|
|
return `
|
|
<div class="editor-field">
|
|
<label>Brand Story Text</label>
|
|
<textarea onchange="composer.updateModule(${index}, 'text', this.value)">${module.data.text}</textarea>
|
|
</div>
|
|
<div class="editor-field">
|
|
<label>Hero Image</label>
|
|
<select onchange="composer.updateModule(${index}, 'heroIndex', this.value)">
|
|
<option value="-1">None</option>
|
|
${this.uploadedImages.map((img, i) => `<option value="${i}" ${module.data.heroIndex == i ? 'selected' : ''}>${img.name}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
`;
|
|
case 'comparison':
|
|
return `
|
|
<div id="comp-${index}">
|
|
${module.data.rows.map((row, ri) => `
|
|
<div class="comparison-row">
|
|
<input type="text" placeholder="Attribute" value="${row.attr}" onchange="composer.updateComparisonRow(${index}, ${ri}, 'attr', this.value)">
|
|
<input type="text" placeholder="Product A" value="${row.vals[0]}" onchange="composer.updateComparisonRow(${index}, ${ri}, 0, this.value)">
|
|
<input type="text" placeholder="Product B" value="${row.vals[1]}" onchange="composer.updateComparisonRow(${index}, ${ri}, 1, this.value)">
|
|
<input type="text" placeholder="Product C" value="${row.vals[2]}" onchange="composer.updateComparisonRow(${index}, ${ri}, 2, this.value)">
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<button class="add-btn" onclick="composer.addComparisonRow(${index})">Add Row</button>
|
|
`;
|
|
case 'faq':
|
|
return `
|
|
<div id="faq-${index}">
|
|
${module.data.pairs.map((pair, pi) => `
|
|
<div class="faq-pair">
|
|
<input type="text" placeholder="Question" value="${pair.q}" onchange="composer.updateFaqPair(${index}, ${pi}, 'q', this.value)" style="width:100%; margin-bottom:5px;">
|
|
<textarea placeholder="Answer" onchange="composer.updateFaqPair(${index}, ${pi}, 'a', this.value)" style="width:100%;">${pair.a}</textarea>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<button class="add-btn" onclick="composer.addFaqPair(${index})">Add Q&A</button>
|
|
`;
|
|
case 'video':
|
|
return `
|
|
<div class="editor-field">
|
|
<label>Video URL</label>
|
|
<input type="url" placeholder="https://..." value="${module.data.url}" onchange="composer.updateModule(${index}, 'url', this.value)">
|
|
</div>
|
|
<div class="editor-field">
|
|
<label>Or Upload Video File</label>
|
|
<input type="file" id="videoUpload-${index}" accept="video/*" style="display:none;" onchange="composer.uploadVideoFile(${index}, this)">
|
|
<button class="upload-video-btn" onclick="document.getElementById('videoUpload-${index}').click()">Choose Video File</button>
|
|
${module.data.videoData ? '<div style="color:#28a745; font-size:12px; margin-top:5px;">✓ Video uploaded</div>' : ''}
|
|
</div>
|
|
<div class="editor-field">
|
|
<label>Thumbnail Image</label>
|
|
<select onchange="composer.updateModule(${index}, 'thumbIndex', this.value)">
|
|
<option value="-1">None</option>
|
|
${this.uploadedImages.map((img, i) => `<option value="${i}" ${module.data.thumbIndex == i ? 'selected' : ''}>${img.name}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
`;
|
|
case 'features':
|
|
return `
|
|
<div id="feat-${index}">
|
|
${module.data.items.map((item, fi) => `
|
|
<div class="feature-item-edit">
|
|
<input type="text" placeholder="Icon" value="${item.icon}" onchange="composer.updateFeatureItem(${index}, ${fi}, 'icon', this.value)" style="width:60px;">
|
|
<input type="text" placeholder="Headline" value="${item.headline}" onchange="composer.updateFeatureItem(${index}, ${fi}, 'headline', this.value)" style="flex:1;">
|
|
<textarea placeholder="Description" onchange="composer.updateFeatureItem(${index}, ${fi}, 'text', this.value)" style="flex:2; min-height:50px;">${item.text}</textarea>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<button class="add-btn" onclick="composer.addFeatureItem(${index})">Add Feature</button>
|
|
`;
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
toggleEditor(index) {
|
|
const editor = document.getElementById(`editor-${this.modules[index].id}`);
|
|
editor.classList.toggle('visible');
|
|
}
|
|
|
|
updateModule(index, field, value) {
|
|
this.modules[index].data[field] = value;
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
updateComparisonRow(modIndex, rowIndex, field, value) {
|
|
if (field === 'attr') {
|
|
this.modules[modIndex].data.rows[rowIndex].attr = value;
|
|
} else {
|
|
this.modules[modIndex].data.rows[rowIndex].vals[field] = value;
|
|
}
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
addComparisonRow(index) {
|
|
this.modules[index].data.rows.push({ attr: '', vals: ['', '', ''] });
|
|
this.renderStack();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
updateFaqPair(modIndex, pairIndex, field, value) {
|
|
this.modules[modIndex].data.pairs[pairIndex][field] = value;
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
addFaqPair(index) {
|
|
this.modules[index].data.pairs.push({ q: '', a: '' });
|
|
this.renderStack();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
updateFeatureItem(modIndex, itemIndex, field, value) {
|
|
this.modules[modIndex].data.items[itemIndex][field] = value;
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
addFeatureItem(index) {
|
|
this.modules[index].data.items.push({ icon: '⭐', headline: '', text: '' });
|
|
this.renderStack();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
uploadVideoFile(index, input) {
|
|
const file = input.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
this.modules[index].data.videoData = e.target.result;
|
|
this.modules[index].data.url = ''; // Clear URL when uploading file
|
|
this.renderStack();
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
}
|
|
|
|
moveUp(index) {
|
|
if (index > 0) {
|
|
[this.modules[index], this.modules[index - 1]] = [this.modules[index - 1], this.modules[index]];
|
|
this.renderStack();
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
}
|
|
|
|
moveDown(index) {
|
|
if (index < this.modules.length - 1) {
|
|
[this.modules[index], this.modules[index + 1]] = [this.modules[index + 1], this.modules[index]];
|
|
this.renderStack();
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
}
|
|
|
|
removeModule(index) {
|
|
this.modules.splice(index, 1);
|
|
this.renderStack();
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
renderModules() {
|
|
const mockup = document.getElementById('btfMockup');
|
|
if (!this.modules.length) {
|
|
mockup.innerHTML = '<div style="text-align:center; color:#999; padding:40px;">No modules added yet</div>';
|
|
return;
|
|
}
|
|
|
|
mockup.innerHTML = '';
|
|
this.modules.forEach(module => {
|
|
const div = document.createElement('div');
|
|
div.className = 'btf-module';
|
|
div.innerHTML = this.renderModule(module);
|
|
mockup.appendChild(div);
|
|
});
|
|
}
|
|
|
|
renderModule(module) {
|
|
switch(module.type) {
|
|
case 'brandStory':
|
|
return `
|
|
<h4>Brand Story</h4>
|
|
<div class="brand-story-hero">
|
|
${module.data.heroIndex >= 0 && this.uploadedImages[module.data.heroIndex]
|
|
? `<img src="${this.uploadedImages[module.data.heroIndex].data}">`
|
|
: 'Hero image'}
|
|
</div>
|
|
<div class="brand-story-text">${module.data.text || 'Brand story text...'}</div>
|
|
`;
|
|
case 'comparison':
|
|
return `
|
|
<h4>Comparison Table</h4>
|
|
<table class="comparison-table">
|
|
${module.data.rows.map((row, i) => `
|
|
<tr>
|
|
<td>${row.attr || `Attribute ${i+1}`}</td>
|
|
${row.vals.map(v => `<td>${v || '-'}</td>`).join('')}
|
|
</tr>
|
|
`).join('')}
|
|
</table>
|
|
`;
|
|
case 'faq':
|
|
return `
|
|
<h4>FAQ</h4>
|
|
${module.data.pairs.map(pair => `
|
|
<div class="faq-item">
|
|
<div class="faq-question">${pair.q || 'Question...'}</div>
|
|
<div class="faq-answer">${pair.a || 'Answer...'}</div>
|
|
</div>
|
|
`).join('')}
|
|
`;
|
|
case 'video':
|
|
let videoContent = 'Video placeholder';
|
|
if (module.data.videoData) {
|
|
videoContent = `<video controls style="width:100%; height:100%; border-radius:8px;"><source src="${module.data.videoData}">Your browser does not support the video tag.</video>`;
|
|
} else if (module.data.thumbIndex >= 0 && this.uploadedImages[module.data.thumbIndex]) {
|
|
videoContent = `<img src="${this.uploadedImages[module.data.thumbIndex].data}">`;
|
|
} else if (module.data.url) {
|
|
videoContent = `Video: ${module.data.url}`;
|
|
}
|
|
return `
|
|
<h4>Product Video</h4>
|
|
<div class="video-placeholder">
|
|
${videoContent}
|
|
</div>
|
|
`;
|
|
case 'features':
|
|
return `
|
|
<h4>Key Features</h4>
|
|
<div class="features-section">
|
|
${module.data.items.map(item => `
|
|
<div class="feature-item">
|
|
<div class="feature-icon">${item.icon || '⭐'}</div>
|
|
<div class="feature-content">
|
|
<div class="feature-headline">${item.headline || 'Feature headline...'}</div>
|
|
<div class="feature-text">${item.text || 'Feature description...'}</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
saveToLocalStorage() {
|
|
const ean = this.ean || document.getElementById('eanInput').value || 'default';
|
|
const data = {
|
|
modules: this.modules,
|
|
uploadedImages: this.uploadedImages,
|
|
ean: ean,
|
|
timestamp: Date.now()
|
|
};
|
|
localStorage.setItem(`btf_${ean}`, JSON.stringify(data));
|
|
}
|
|
|
|
loadFromLocalStorage() {
|
|
const ean = this.ean || document.getElementById('eanInput').value;
|
|
if (!ean) {
|
|
// Don't auto-load if no EAN is set - start clean
|
|
return;
|
|
}
|
|
const stored = localStorage.getItem(`btf_${ean}`);
|
|
if (stored) {
|
|
const data = JSON.parse(stored);
|
|
this.modules = data.modules || [];
|
|
this.uploadedImages = data.uploadedImages || [];
|
|
this.ean = data.ean || '';
|
|
document.getElementById('eanInput').value = this.ean;
|
|
this.updateImagePreview();
|
|
this.renderStack();
|
|
this.renderModules();
|
|
}
|
|
}
|
|
|
|
exportJSON() {
|
|
const data = {
|
|
modules: this.modules,
|
|
uploadedImages: this.uploadedImages,
|
|
ean: this.ean || document.getElementById('eanInput').value,
|
|
timestamp: Date.now()
|
|
};
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `btf_${data.ean || 'export'}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
importJSON() {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.json';
|
|
input.onchange = (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
try {
|
|
const data = JSON.parse(ev.target.result);
|
|
this.modules = data.modules || [];
|
|
this.uploadedImages = data.uploadedImages || [];
|
|
this.ean = data.ean || '';
|
|
document.getElementById('eanInput').value = this.ean;
|
|
this.updateImagePreview();
|
|
this.renderStack();
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
} catch (err) {
|
|
alert('Invalid JSON file');
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
};
|
|
input.click();
|
|
}
|
|
|
|
async exportBTF() {
|
|
const mockup = document.getElementById('btfMockup');
|
|
try {
|
|
const canvas = await html2canvas(mockup, { backgroundColor: '#ffffff', scale: 2 });
|
|
const ean = this.ean || document.getElementById('eanInput').value || 'btf';
|
|
const url = canvas.toDataURL('image/jpeg', 0.92);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${ean}_BTF_AMZ.jpg`;
|
|
a.click();
|
|
} catch (err) {
|
|
alert('Export failed: ' + err.message);
|
|
}
|
|
}
|
|
|
|
exportCSV() {
|
|
if (!this.modules.length) {
|
|
alert('No modules to export');
|
|
return;
|
|
}
|
|
|
|
const rows = [];
|
|
|
|
this.modules.forEach(module => {
|
|
switch(module.type) {
|
|
case 'brandStory':
|
|
if (module.data.text) {
|
|
rows.push(['premium_brand_story_text', '', this.escapeCSV(module.data.text)]);
|
|
}
|
|
break;
|
|
case 'comparison':
|
|
module.data.rows.forEach((row, i) => {
|
|
const idx = i + 1;
|
|
rows.push([`premium_comparison_attr_${idx}`, '', this.escapeCSV(row.attr)]);
|
|
row.vals.forEach((val, skuIdx) => {
|
|
rows.push([`premium_comparison_sku_${skuIdx + 1}_attr_${idx}`, '', this.escapeCSV(val)]);
|
|
});
|
|
});
|
|
break;
|
|
case 'faq':
|
|
module.data.pairs.forEach((pair, i) => {
|
|
const idx = i + 1;
|
|
rows.push([`premium_faq_question_${idx}`, '', this.escapeCSV(pair.q)]);
|
|
rows.push([`premium_faq_answer_${idx}`, '', this.escapeCSV(pair.a)]);
|
|
});
|
|
break;
|
|
case 'video':
|
|
if (module.data.url) {
|
|
rows.push(['premium_video_url', '', this.escapeCSV(module.data.url)]);
|
|
}
|
|
if (module.data.thumbIndex >= 0) {
|
|
rows.push(['premium_video_thumbnail_index', '', module.data.thumbIndex]);
|
|
}
|
|
break;
|
|
case 'features':
|
|
module.data.items.forEach((item, i) => {
|
|
const idx = i + 1;
|
|
rows.push([`premium_feature_${idx}_icon`, '', this.escapeCSV(item.icon)]);
|
|
rows.push([`premium_feature_${idx}_headline`, '', this.escapeCSV(item.headline)]);
|
|
rows.push([`premium_feature_${idx}_text`, '', this.escapeCSV(item.text)]);
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
|
|
const csvContent = rows.map(row => row.join(',')).join('\n');
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
const ean = this.ean || document.getElementById('eanInput').value || 'btf';
|
|
a.download = `${ean}_btf_content.csv`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
escapeCSV(str) {
|
|
if (!str) return '';
|
|
const s = String(str);
|
|
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
|
return `"${s.replace(/"/g, '""')}"`;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
toggleViewport(mode) {
|
|
const mockup = document.getElementById('btfMockup');
|
|
if (mode === 'mobile') {
|
|
mockup.classList.add('mobile');
|
|
} else {
|
|
mockup.classList.remove('mobile');
|
|
}
|
|
}
|
|
|
|
clearAll() {
|
|
if (confirm('Clear all content, images, and CSV data? This cannot be undone.')) {
|
|
this.modules = [];
|
|
this.uploadedImages = [];
|
|
this.csvData = {};
|
|
this.ean = '';
|
|
document.getElementById('eanInput').value = '';
|
|
document.getElementById('viewportToggle').value = 'desktop';
|
|
this.updateImagePreview();
|
|
this.updateCsvPreview();
|
|
this.renderStack();
|
|
this.renderModules();
|
|
// Clear localStorage for current EAN
|
|
const ean = document.getElementById('eanInput').value || 'default';
|
|
localStorage.removeItem(`btf_${ean}`);
|
|
}
|
|
}
|
|
|
|
autoFillExistingModules() {
|
|
// Auto-fill existing modules with CSV data
|
|
this.modules.forEach((module, index) => {
|
|
switch(module.type) {
|
|
case 'brandStory':
|
|
if (this.csvData['premium_brand_story_text']) {
|
|
module.data.text = this.csvData['premium_brand_story_text'];
|
|
}
|
|
break;
|
|
case 'comparison':
|
|
// Auto-fill comparison from CSV
|
|
const skuNames = [];
|
|
for (let i = 1; i <= 3; i++) {
|
|
const name = this.csvData[`premium_comparison_sku_${i}_name`];
|
|
if (name) skuNames.push(name);
|
|
}
|
|
if (skuNames.length) {
|
|
module.data.rows = [];
|
|
// Header row
|
|
let attrCount = 1;
|
|
while (this.csvData[`premium_comparison_attr_${attrCount}`]) {
|
|
const attr = this.csvData[`premium_comparison_attr_${attrCount}`];
|
|
const vals = [];
|
|
for (let i = 1; i <= 3; i++) {
|
|
vals.push(this.csvData[`premium_comparison_sku_${i}_attr_${attrCount}`] || '');
|
|
}
|
|
module.data.rows.push({ attr, vals });
|
|
attrCount++;
|
|
}
|
|
}
|
|
break;
|
|
case 'faq':
|
|
const pairs = [];
|
|
let faqIdx = 1;
|
|
while (this.csvData[`premium_faq_question_${faqIdx}`]) {
|
|
pairs.push({
|
|
q: this.csvData[`premium_faq_question_${faqIdx}`] || '',
|
|
a: this.csvData[`premium_faq_answer_${faqIdx}`] || ''
|
|
});
|
|
faqIdx++;
|
|
}
|
|
if (pairs.length) module.data.pairs = pairs;
|
|
break;
|
|
case 'video':
|
|
if (this.csvData['premium_video_url']) {
|
|
module.data.url = this.csvData['premium_video_url'];
|
|
}
|
|
break;
|
|
case 'features':
|
|
const items = [];
|
|
let featIdx = 1;
|
|
while (this.csvData[`premium_feature_${featIdx}_headline`]) {
|
|
items.push({
|
|
icon: this.csvData[`premium_feature_${featIdx}_icon`] || '⭐',
|
|
headline: this.csvData[`premium_feature_${featIdx}_headline`] || '',
|
|
text: this.csvData[`premium_feature_${featIdx}_text`] || ''
|
|
});
|
|
featIdx++;
|
|
}
|
|
if (items.length) module.data.items = items;
|
|
break;
|
|
}
|
|
});
|
|
this.renderStack();
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
|
|
autoPopulateFromCSV() {
|
|
if (!Object.keys(this.csvData).length) {
|
|
alert('Please upload a CSV file first');
|
|
return;
|
|
}
|
|
|
|
// Brand Story
|
|
if (this.csvData['premium_brand_story_text']) {
|
|
const exists = this.modules.find(m => m.type === 'brandStory');
|
|
if (!exists) {
|
|
this.addModule('brandStory');
|
|
const mod = this.modules[this.modules.length - 1];
|
|
mod.data.text = this.csvData['premium_brand_story_text'];
|
|
}
|
|
}
|
|
|
|
// Comparison
|
|
if (this.csvData['premium_comparison_sku_1_name']) {
|
|
const exists = this.modules.find(m => m.type === 'comparison');
|
|
if (!exists) {
|
|
this.addModule('comparison');
|
|
const mod = this.modules[this.modules.length - 1];
|
|
mod.data.rows = [];
|
|
let attrCount = 1;
|
|
while (this.csvData[`premium_comparison_attr_${attrCount}`]) {
|
|
const attr = this.csvData[`premium_comparison_attr_${attrCount}`];
|
|
const vals = [];
|
|
for (let i = 1; i <= 3; i++) {
|
|
vals.push(this.csvData[`premium_comparison_sku_${i}_attr_${attrCount}`] || '');
|
|
}
|
|
mod.data.rows.push({ attr, vals });
|
|
attrCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// FAQ
|
|
if (this.csvData['premium_faq_question_1']) {
|
|
const exists = this.modules.find(m => m.type === 'faq');
|
|
if (!exists) {
|
|
this.addModule('faq');
|
|
const mod = this.modules[this.modules.length - 1];
|
|
mod.data.pairs = [];
|
|
let faqIdx = 1;
|
|
while (this.csvData[`premium_faq_question_${faqIdx}`]) {
|
|
mod.data.pairs.push({
|
|
q: this.csvData[`premium_faq_question_${faqIdx}`] || '',
|
|
a: this.csvData[`premium_faq_answer_${faqIdx}`] || ''
|
|
});
|
|
faqIdx++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Video
|
|
if (this.csvData['premium_video_url']) {
|
|
const exists = this.modules.find(m => m.type === 'video');
|
|
if (!exists) {
|
|
this.addModule('video');
|
|
const mod = this.modules[this.modules.length - 1];
|
|
mod.data.url = this.csvData['premium_video_url'];
|
|
}
|
|
}
|
|
|
|
// Features
|
|
if (this.csvData['premium_feature_1_headline']) {
|
|
const exists = this.modules.find(m => m.type === 'features');
|
|
if (!exists) {
|
|
this.addModule('features');
|
|
const mod = this.modules[this.modules.length - 1];
|
|
mod.data.items = [];
|
|
let featIdx = 1;
|
|
while (this.csvData[`premium_feature_${featIdx}_headline`]) {
|
|
mod.data.items.push({
|
|
icon: this.csvData[`premium_feature_${featIdx}_icon`] || '⭐',
|
|
headline: this.csvData[`premium_feature_${featIdx}_headline`] || '',
|
|
text: this.csvData[`premium_feature_${featIdx}_text`] || ''
|
|
});
|
|
featIdx++;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.renderStack();
|
|
this.renderModules();
|
|
this.saveToLocalStorage();
|
|
}
|
|
}
|
|
|
|
const composer = new BTFComposer();
|
|
</script>
|
|
</body>
|
|
</html>
|