loreal_ecf/BTF_composer.html

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>