initial commit

This commit is contained in:
Alessandro Benedetti Admin 2025-12-16 15:53:33 +00:00
commit a0940134d4
28 changed files with 4102 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View file

@ -0,0 +1,10 @@
DMI_Product_type,,L'Oréal Professionnel
Amazon - Product Name (Long),,L'Oréal Professionnel Curl Expression Leave-in hidratante intensivo de longa duração SERIE EXPERT 200ml Para todos os tipos de caracóis e ondas
Amazon Benefits,,Hidrata profundamente
Amazon - Bullet Features 1,,Hidratação de longa duração
Amazon - Bullet Features 2,,O leave-in hidratante de longa duração Curl Expression para ondas e caracóis
Amazon - Bullet Features 3,,Serie Expert Curl Expression, Expressa os teus caracóis de forma pro.
Amazon - Bullet Features 4,,Quando necessário
Amazon - Bullet Features 5,,Serie Expert
amazon_product_description_short,,[CUIDE OS SEUS CARACÓIS COMO UM PRO]: O leave-in hidratante de longa duração Curl Expression protege o cabelo até 230°C/450F com uma fórmula untuosa duradoura sem adicionar peso ao cabelo, de forma profissional. [ATIVOS ALTAMENTE CONCENTRADOS]: Com glicerina à base de plantas e Ureia HCom Glicerina à base de plantas, Ureia H e semente de hibisco para uns caracóis e ondas fortes e hidratadas. [EXPERIÊNCIA ÚNICA]: Esta fórmula sem parabenos deixa os caracóis e ondas intensamente hidratados e fortalecidos. [CODESENVOLVIDO COM ESPECIALISTAS EM CARACÓIS]: Com 4 especialistas internacionais em ondas e caracóis: Titus Magida da África do Sul(@call.me.titus), Vivi Siqueira do Brazil (@vivi_siqueira), Derick Monroe dos Estados Unidos (@derickmonroe) e Elodie Euston da França (@elodieeuston).
amazon_instruction_of_use,,Com o cabelo húmido, aplicar com os dedos desde os comprimentos até às pontas.
1 DMI_Product_type,,L'Oréal Professionnel
2 Amazon - Product Name (Long),,L'Oréal Professionnel Curl Expression Leave-in hidratante intensivo de longa duração SERIE EXPERT 200ml Para todos os tipos de caracóis e ondas
3 Amazon Benefits,,Hidrata profundamente
4 Amazon - Bullet Features 1,,Hidratação de longa duração
5 Amazon - Bullet Features 2,,O leave-in hidratante de longa duração Curl Expression para ondas e caracóis
6 Amazon - Bullet Features 3,,Serie Expert Curl Expression, Expressa os teus caracóis de forma pro.
7 Amazon - Bullet Features 4,,Quando necessário
8 Amazon - Bullet Features 5,,Serie Expert
9 amazon_product_description_short,,[CUIDE OS SEUS CARACÓIS COMO UM PRO]: O leave-in hidratante de longa duração Curl Expression protege o cabelo até 230°C/450F com uma fórmula untuosa duradoura sem adicionar peso ao cabelo, de forma profissional. [ATIVOS ALTAMENTE CONCENTRADOS]: Com glicerina à base de plantas e Ureia HCom Glicerina à base de plantas, Ureia H e semente de hibisco para uns caracóis e ondas fortes e hidratadas. [EXPERIÊNCIA ÚNICA]: Esta fórmula sem parabenos deixa os caracóis e ondas intensamente hidratados e fortalecidos. [CODESENVOLVIDO COM ESPECIALISTAS EM CARACÓIS]: Com 4 especialistas internacionais em ondas e caracóis: Titus Magida da África do Sul(@call.me.titus), Vivi Siqueira do Brazil (@vivi_siqueira), Derick Monroe dos Estados Unidos (@derickmonroe) e Elodie Euston da França (@elodieeuston).
10 amazon_instruction_of_use,,Com o cabelo húmido, aplicar com os dedos desde os comprimentos até às pontas.

959
Asset/btf_composer.html Executable file
View file

@ -0,0 +1,959 @@
<!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,
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.ean = data.ean || '';
document.getElementById('eanInput').value = this.ean;
this.renderStack();
this.renderModules();
}
}
exportJSON() {
const data = {
modules: this.modules,
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.ean = data.ean || '';
document.getElementById('eanInput').value = this.ean;
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>

44
Asset/sample_premium.csv Executable file
View file

@ -0,0 +1,44 @@
DMI_Product_type,,L'Oréal Professionnel
Amazon - Product Name (Long),,L'Oréal Professionnel Curl Expression Leave-in hidratante intensivo de longa duração SERIE EXPERT 200ml Para todos os tipos de caracóis e ondas
Amazon Benefits,,Hidrata profundamente
Amazon - Bullet Features 1,,Hidratação de longa duração
Amazon - Bullet Features 2,,O leave-in hidratante de longa duração Curl Expression para ondas e caracóis
Amazon - Bullet Features 3,,Serie Expert Curl Expression, Expressa os teus caracóis de forma pro.
Amazon - Bullet Features 4,,Quando necessário
Amazon - Bullet Features 5,,Serie Expert
amazon_product_description_short,,[CUIDE OS SEUS CARACÓIS COMO UM PRO]: O leave-in hidratante de longa duração Curl Expression protege o cabelo até 230°C/450F com uma fórmula untuosa duradoura sem adicionar peso ao cabelo, de forma profissional. [ATIVOS ALTAMENTE CONCENTRADOS]: Com glicerina à base de plantas e Ureia HCom Glicerina à base de plantas, Ureia H e semente de hibisco para uns caracóis e ondas fortes e hidratadas. [EXPERIÊNCIA ÚNICA]: Esta fórmula sem parabenos deixa os caracóis e ondas intensamente hidratados e fortalecidos. [CODESENVOLVIDO COM ESPECIALISTAS EM CARACÓIS]: Com 4 especialistas internacionais em ondas e caracóis: Titus Magida da África do Sul(@call.me.titus), Vivi Siqueira do Brazil (@vivi_siqueira), Derick Monroe dos Estados Unidos (@derickmonroe) e Elodie Euston da França (@elodieeuston).
amazon_instruction_of_use,,Com o cabelo húmido, aplicar com os dedos desde os comprimentos até às pontas.
premium_brand_story_text,,L'Oréal Professionnel has been at the forefront of hair care innovation for over 100 years. Our Curl Expression line represents the culmination of decades of research into curly hair needs, developed in partnership with curl specialists worldwide. This leave-in treatment combines cutting-edge science with natural ingredients to deliver professional results at home.
premium_comparison_sku_1_name,,Curl Expression 200ml
premium_comparison_sku_2_name,,Curl Expression 100ml
premium_comparison_sku_3_name,,Curl Expression 300ml
premium_comparison_attr_1,,Size
premium_comparison_sku_1_attr_1,,200ml
premium_comparison_sku_2_attr_1,,100ml
premium_comparison_sku_3_attr_1,,300ml
premium_comparison_attr_2,,Price
premium_comparison_sku_1_attr_2,,$24.99
premium_comparison_sku_2_attr_2,,$14.99
premium_comparison_sku_3_attr_2,,$34.99
premium_comparison_attr_3,,Heat Protection
premium_comparison_sku_1_attr_3,,Up to 230°C
premium_comparison_sku_2_attr_3,,Up to 230°C
premium_comparison_sku_3_attr_3,,Up to 230°C
premium_comparison_attr_4,,Hair Types
premium_comparison_sku_1_attr_4,,All curl types
premium_comparison_sku_2_attr_4,,All curl types
premium_comparison_sku_3_attr_4,,All curl types
premium_comparison_attr_5,,Key Ingredient
premium_comparison_sku_1_attr_5,,3% Plant Glycerin
premium_comparison_sku_2_attr_5,,3% Plant Glycerin
premium_comparison_sku_3_attr_5,,3% Plant Glycerin
premium_faq_question_1,,How often should I use this product?
premium_faq_answer_1,,Use as needed, typically 2-3 times per week or whenever your curls need extra hydration and definition.
premium_faq_question_2,,Can I use this on color-treated hair?
premium_faq_answer_2,,Yes, this product is safe for color-treated hair and won't strip or fade your color.
premium_faq_question_3,,What's the difference between this and regular conditioner?
premium_faq_answer_3,,This is a leave-in treatment that provides continuous hydration and heat protection, while conditioner is rinsed out and provides temporary conditioning.
premium_faq_question_4,,Is this product suitable for fine hair?
premium_faq_answer_4,,Yes, the lightweight formula won't weigh down fine hair while still providing the hydration and definition your curls need.
premium_faq_question_5,,How long does one bottle last?
premium_faq_answer_5,,A 200ml bottle typically lasts 2-3 months with regular use, depending on hair length and thickness.
1 DMI_Product_type,,L'Oréal Professionnel
2 Amazon - Product Name (Long),,L'Oréal Professionnel Curl Expression Leave-in hidratante intensivo de longa duração SERIE EXPERT 200ml Para todos os tipos de caracóis e ondas
3 Amazon Benefits,,Hidrata profundamente
4 Amazon - Bullet Features 1,,Hidratação de longa duração
5 Amazon - Bullet Features 2,,O leave-in hidratante de longa duração Curl Expression para ondas e caracóis
6 Amazon - Bullet Features 3,,Serie Expert Curl Expression, Expressa os teus caracóis de forma pro.
7 Amazon - Bullet Features 4,,Quando necessário
8 Amazon - Bullet Features 5,,Serie Expert
9 amazon_product_description_short,,[CUIDE OS SEUS CARACÓIS COMO UM PRO]: O leave-in hidratante de longa duração Curl Expression protege o cabelo até 230°C/450F com uma fórmula untuosa duradoura sem adicionar peso ao cabelo, de forma profissional. [ATIVOS ALTAMENTE CONCENTRADOS]: Com glicerina à base de plantas e Ureia HCom Glicerina à base de plantas, Ureia H e semente de hibisco para uns caracóis e ondas fortes e hidratadas. [EXPERIÊNCIA ÚNICA]: Esta fórmula sem parabenos deixa os caracóis e ondas intensamente hidratados e fortalecidos. [CODESENVOLVIDO COM ESPECIALISTAS EM CARACÓIS]: Com 4 especialistas internacionais em ondas e caracóis: Titus Magida da África do Sul(@call.me.titus), Vivi Siqueira do Brazil (@vivi_siqueira), Derick Monroe dos Estados Unidos (@derickmonroe) e Elodie Euston da França (@elodieeuston).
10 amazon_instruction_of_use,,Com o cabelo húmido, aplicar com os dedos desde os comprimentos até às pontas.
11 premium_brand_story_text,,L'Oréal Professionnel has been at the forefront of hair care innovation for over 100 years. Our Curl Expression line represents the culmination of decades of research into curly hair needs, developed in partnership with curl specialists worldwide. This leave-in treatment combines cutting-edge science with natural ingredients to deliver professional results at home.
12 premium_comparison_sku_1_name,,Curl Expression 200ml
13 premium_comparison_sku_2_name,,Curl Expression 100ml
14 premium_comparison_sku_3_name,,Curl Expression 300ml
15 premium_comparison_attr_1,,Size
16 premium_comparison_sku_1_attr_1,,200ml
17 premium_comparison_sku_2_attr_1,,100ml
18 premium_comparison_sku_3_attr_1,,300ml
19 premium_comparison_attr_2,,Price
20 premium_comparison_sku_1_attr_2,,$24.99
21 premium_comparison_sku_2_attr_2,,$14.99
22 premium_comparison_sku_3_attr_2,,$34.99
23 premium_comparison_attr_3,,Heat Protection
24 premium_comparison_sku_1_attr_3,,Up to 230°C
25 premium_comparison_sku_2_attr_3,,Up to 230°C
26 premium_comparison_sku_3_attr_3,,Up to 230°C
27 premium_comparison_attr_4,,Hair Types
28 premium_comparison_sku_1_attr_4,,All curl types
29 premium_comparison_sku_2_attr_4,,All curl types
30 premium_comparison_sku_3_attr_4,,All curl types
31 premium_comparison_attr_5,,Key Ingredient
32 premium_comparison_sku_1_attr_5,,3% Plant Glycerin
33 premium_comparison_sku_2_attr_5,,3% Plant Glycerin
34 premium_comparison_sku_3_attr_5,,3% Plant Glycerin
35 premium_faq_question_1,,How often should I use this product?
36 premium_faq_answer_1,,Use as needed, typically 2-3 times per week or whenever your curls need extra hydration and definition.
37 premium_faq_question_2,,Can I use this on color-treated hair?
38 premium_faq_answer_2,,Yes, this product is safe for color-treated hair and won't strip or fade your color.
39 premium_faq_question_3,,What's the difference between this and regular conditioner?
40 premium_faq_answer_3,,This is a leave-in treatment that provides continuous hydration and heat protection, while conditioner is rinsed out and provides temporary conditioning.
41 premium_faq_question_4,,Is this product suitable for fine hair?
42 premium_faq_answer_4,,Yes, the lightweight formula won't weigh down fine hair while still providing the hydration and definition your curls need.
43 premium_faq_question_5,,How long does one bottle last?
44 premium_faq_answer_5,,A 200ml bottle typically lasts 2-3 months with regular use, depending on hair length and thickness.

BIN
BTF_Composer.zip Executable file

Binary file not shown.

10
LP_CURL_EXPRESSIONS_for_app.csv Executable file
View file

@ -0,0 +1,10 @@
DMI_Product_type,,L'Oréal Professionnel
Amazon - Product Name (Long),,L'Oréal Professionnel Curl Expression Leave-in hidratante intensivo de longa duração SERIE EXPERT 200ml Para todos os tipos de caracóis e ondas
Amazon Benefits,,Hidrata profundamente
Amazon - Bullet Features 1,,Hidratação de longa duração
Amazon - Bullet Features 2,,O leave-in hidratante de longa duração Curl Expression para ondas e caracóis
Amazon - Bullet Features 3,,Serie Expert Curl Expression, Expressa os teus caracóis de forma pro.
Amazon - Bullet Features 4,,Quando necessário
Amazon - Bullet Features 5,,Serie Expert
amazon_product_description_short,,[CUIDE OS SEUS CARACÓIS COMO UM PRO]: O leave-in hidratante de longa duração Curl Expression protege o cabelo até 230°C/450F com uma fórmula untuosa duradoura sem adicionar peso ao cabelo, de forma profissional. [ATIVOS ALTAMENTE CONCENTRADOS]: Com glicerina à base de plantas e Ureia HCom Glicerina à base de plantas, Ureia H e semente de hibisco para uns caracóis e ondas fortes e hidratadas. [EXPERIÊNCIA ÚNICA]: Esta fórmula sem parabenos deixa os caracóis e ondas intensamente hidratados e fortalecidos. [CODESENVOLVIDO COM ESPECIALISTAS EM CARACÓIS]: Com 4 especialistas internacionais em ondas e caracóis: Titus Magida da África do Sul(@call.me.titus), Vivi Siqueira do Brazil (@vivi_siqueira), Derick Monroe dos Estados Unidos (@derickmonroe) e Elodie Euston da França (@elodieeuston).
amazon_instruction_of_use,,Com o cabelo húmido, aplicar com os dedos desde os comprimentos até às pontas.
1 DMI_Product_type,,L'Oréal Professionnel
2 Amazon - Product Name (Long),,L'Oréal Professionnel Curl Expression Leave-in hidratante intensivo de longa duração SERIE EXPERT 200ml Para todos os tipos de caracóis e ondas
3 Amazon Benefits,,Hidrata profundamente
4 Amazon - Bullet Features 1,,Hidratação de longa duração
5 Amazon - Bullet Features 2,,O leave-in hidratante de longa duração Curl Expression para ondas e caracóis
6 Amazon - Bullet Features 3,,Serie Expert Curl Expression, Expressa os teus caracóis de forma pro.
7 Amazon - Bullet Features 4,,Quando necessário
8 Amazon - Bullet Features 5,,Serie Expert
9 amazon_product_description_short,,[CUIDE OS SEUS CARACÓIS COMO UM PRO]: O leave-in hidratante de longa duração Curl Expression protege o cabelo até 230°C/450F com uma fórmula untuosa duradoura sem adicionar peso ao cabelo, de forma profissional. [ATIVOS ALTAMENTE CONCENTRADOS]: Com glicerina à base de plantas e Ureia HCom Glicerina à base de plantas, Ureia H e semente de hibisco para uns caracóis e ondas fortes e hidratadas. [EXPERIÊNCIA ÚNICA]: Esta fórmula sem parabenos deixa os caracóis e ondas intensamente hidratados e fortalecidos. [CODESENVOLVIDO COM ESPECIALISTAS EM CARACÓIS]: Com 4 especialistas internacionais em ondas e caracóis: Titus Magida da África do Sul(@call.me.titus), Vivi Siqueira do Brazil (@vivi_siqueira), Derick Monroe dos Estados Unidos (@derickmonroe) e Elodie Euston da França (@elodieeuston).
10 amazon_instruction_of_use,,Com o cabelo húmido, aplicar com os dedos desde os comprimentos até às pontas.

123
README.md Executable file
View file

@ -0,0 +1,123 @@
# Amazon PDP Mockup Generator
A simple web application that generates realistic Amazon Product Detail Page (PDP) mockups by combining uploaded product images with CSV-based copy content.
## Features
- **Drag & Drop Interface**: Upload multiple JPG images by dragging and dropping
- **CSV Data Integration**: Upload CSV files to populate product copy automatically
- **Live Preview**: See your Amazon mockup update in real-time as you add content
- **Image Management**: Click to select hero image, thumbnails auto-populate
- **Export Functionality**: Download your mockup as a high-quality PNG
- **Responsive Design**: Works on desktop, tablet, and mobile devices
## How to Use
### 1. Upload Images
- Drag and drop JPG files onto the "Drop JPG Images Here" area
- Or click the area to browse and select files
- Click any uploaded image to set it as the hero image
- Other images will appear as thumbnails and A+ content
### 2. Upload CSV Data
- Drag and drop a CSV file onto the "Upload CSV Data" area
- Or click the area to browse and select a CSV file
- The CSV should contain product information in the format shown below
### 3. Preview & Export
- Your Amazon mockup will update automatically as you add content
- Click "Export as PNG" to download your final mockup
## CSV Format
Your CSV file should have two columns: `field` and `content`. Here are the supported fields:
### Required Fields
- `brand_name` - The brand name (e.g., "L'Oréal Paris")
- `product_title` - The main product title
- `description` - Product description text
### Optional Fields
- `bullet_1` through `bullet_5` - Key product features (up to 5 bullets)
- `price` - Product price (e.g., "$24.99")
- `rating` - Star rating (e.g., "4.5")
- `review_count` - Number of reviews (e.g., "1234")
### Example CSV Format
```csv
field,content
brand_name,L'Oréal Paris
product_title,Advanced Hair Serum - Keratin Infused 100ml
bullet_1,Professional-grade formula with keratin complex
bullet_2,Suitable for all hair types and textures
bullet_3,Reduces frizz and adds natural shine
bullet_4,Dermatologically tested and approved
bullet_5,Cruelty-free and vegan friendly
description,Transform your hair care routine with our advanced serum...
price,$24.99
rating,4.5
review_count,1234
```
## File Structure
```
amazon-mockup-generator/
├── index.html # Main application (all-in-one)
├── sample.csv # Example CSV template
└── README.md # This documentation
```
## Technical Details
- **Single File Application**: Everything is contained in `index.html`
- **No Server Required**: Open directly in any modern web browser
- **Export Technology**: Uses html2canvas library for PNG export
- **Browser Compatibility**: Works in Chrome, Firefox, Safari, Edge (modern versions)
## Tips for Best Results
### Images
- Use high-quality JPG images (minimum 800x800px recommended)
- First image uploaded will be set as hero image by default
- Click any image to change the hero image
- Images work best with white or transparent backgrounds
### CSV Data
- Use the provided `sample.csv` as a template
- Keep bullet points concise (under 100 characters each)
- Product title should be descriptive but not too long
- Description can be longer and more detailed
### Export
- Export works best when you have both images and CSV data loaded
- The exported PNG will be high-resolution (2x scale)
- Large mockups may take a few seconds to export
## Troubleshooting
### Images Not Loading
- Ensure files are JPG format (not PNG or other formats)
- Check file size (very large files may take time to process)
- Try refreshing the page and uploading again
### CSV Not Working
- Verify your CSV has the correct format (field,content columns)
- Check that field names match exactly (case-sensitive)
- Ensure no special characters in field names
### Export Issues
- Make sure you have content loaded (images or CSV data)
- Try refreshing the page if export fails
- Check browser console for any error messages
## Browser Requirements
- Modern web browser (Chrome 60+, Firefox 55+, Safari 12+, Edge 79+)
- JavaScript enabled
- File API support (for drag & drop)
- Canvas API support (for export)
## Support
This is a standalone application that runs entirely in your browser. No installation or server setup required. Simply open `index.html` in your web browser to get started.

965
btf_composer.html Executable file
View file

@ -0,0 +1,965 @@
<!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>

1046
index.html Executable file

File diff suppressed because it is too large Load diff

292
index_brand.html Executable file
View file

@ -0,0 +1,292 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Amazon PDP Mockup Generator Brand Layout</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: 1400px; 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; }
.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); }
.image-thumb.selected { border-color: #ff9900; box-shadow: 0 0 0 2px rgba(255, 153, 0, 0.3); }
.csv-data { background: #f8f9fa; border-radius: 4px; padding: 15px; font-family: monospace; font-size: 14px; max-height: 200px; overflow-y: auto; }
.mockup-container { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
.mockup-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; gap: 10px; flex-wrap: wrap; }
.export-btn { background: #ff9900; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 600; transition: background 0.3s ease; }
.export-btn:disabled { background: #ccc; cursor: not-allowed; }
.amazon-mockup { background: white; border: 1px solid #ddd; border-radius: 4px; padding: 20px; font-family: Arial, sans-serif; max-width: 100%; }
.amazon-header { background: #232f3e; color: white; padding: 10px 20px; margin: -20px -20px 20px -20px; border-radius: 4px 4px 0 0; }
.product-section { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-bottom: 30px; }
.hero-image { width: 100%; max-width: 400px; height: 400px; object-fit: cover; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa; display: flex; align-items: center; justify-content: center; color: #666; font-size: 14px; cursor: pointer; }
.thumbnail-images { display: flex; gap: 5px; flex-wrap: wrap; }
.thumbnail { width: 60px; height: 60px; object-fit: cover; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; }
.brand-name { color: #0066c0; font-size: 14px; margin-bottom: 5px; }
.product-title { font-size: 20px; font-weight: 400; line-height: 1.3; margin-bottom: 8px; color: #0f1111; }
.subtitle { font-size: 14px; color: #565959; margin-bottom: 12px; }
.bullets { margin-bottom: 20px; }
.bullet { margin-bottom: 8px; font-size: 14px; line-height: 1.4; }
.bullet::before { content: "• "; color: #ff9900; font-weight: bold; }
.description { font-size: 14px; line-height: 1.5; }
.instructions { margin-top: 10px; color: #565959; font-size: 13px; line-height: 1.45; }
.aplus-content { margin-top: 30px; border-top: 1px solid #ddd; padding-top: 20px; }
.aplus-controls { display:flex; gap:10px; align-items:center; margin-bottom:10px; }
.aplus-images { display: grid; gap: 15px; }
.aplus-images.standard { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.aplus-images.large { grid-template-columns: repeat(3, 1fr); }
.aplus-image { width: 100%; aspect-ratio: 1/1; height: auto; border: 1px solid #ddd; border-radius: 4px; background: #ffffff; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; }
.aplus-image img { width: 100%; height: 100%; object-fit: contain; background: #ffffff; }
.order-badge { position: absolute; top: 6px; left: 6px; background: #232f3e; color: #fff; font-size: 12px; padding: 2px 6px; border-radius: 12px; font-weight: 600; }
.toolbar { display:flex; gap:10px; align-items:center; }
@media (max-width: 768px) { .upload-section, .preview-section, .product-section { grid-template-columns: 1fr; } .container { padding: 10px; } }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Amazon PDP Mockup Generator — Brand Layout</h1>
<p>Variant with A+ layout selector and order badges (self-contained)</p>
</div>
<div class="upload-section">
<div class="upload-box" id="imageUpload">
<div class="upload-icon">📸</div>
<div class="upload-text">Drop JPG/PNG 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>
<div id="statusMessage"></div>
<div class="preview-section">
<div class="image-preview">
<h3>Uploaded Images</h3>
<div class="image-grid" id="imageGrid"><div class="empty-state">No images uploaded yet</div></div>
</div>
<div class="csv-preview">
<h3>CSV Data Preview</h3>
<div class="csv-data" id="csvData"><div class="empty-state">No CSV data loaded yet</div></div>
</div>
</div>
<div class="mockup-container">
<div class="mockup-header">
<h3>Amazon Product Page Mockup</h3>
<div class="toolbar">
<label style="display:flex; align-items:center; gap:6px; font-size:14px; color:#232f3e;">
<span>EAN</span>
<input id="eanInput" type="text" inputmode="numeric" pattern="\d{13}" maxlength="13" placeholder="13-digit EAN" style="height:30px; padding:4px 8px; border:1px solid #ddd; border-radius:4px; min-width:180px;" />
</label>
<label style="display:flex; align-items:center; gap:6px; font-size:14px; color:#232f3e;">
<input type="checkbox" id="pinHeroToggle"> Pin hero
</label>
<label style="display:flex; align-items:center; gap:6px; font-size:14px; color:#232f3e;">
<span>A+ layout</span>
<select id="aplusLayoutSelect" style="height:30px; padding:4px 8px; border:1px solid #ddd; border-radius:4px;">
<option value="standard">Standard (auto-fit)</option>
<option value="large">Large 3-up</option>
</select>
</label>
<button class="export-btn" id="downloadAssetsBtn" disabled>Download assets (ZIP)</button>
<button class="export-btn" id="exportBtn" disabled>Export as JPG</button>
</div>
</div>
<div class="amazon-mockup" id="amazonMockup">
<div class="amazon-header"><h2>Amazon.com</h2></div>
<div class="product-section">
<div class="product-images">
<div class="hero-image" id="heroImage">Drop your main product image here</div>
<div class="thumbnail-images" id="thumbnailImages"></div>
</div>
<div class="product-info">
<div class="brand-name" id="brandName">Brand Name</div>
<div class="product-title" id="productTitle">Product Title</div>
<div class="subtitle" id="subtitle" style="display:none;"></div>
<div class="bullets" id="bullets"></div>
<div class="description" id="description">Product description will appear here...</div>
<div class="instructions" id="instructions" style="display:none;"></div>
</div>
</div>
<div class="aplus-content">
<div class="aplus-controls"></div>
<div class="aplus-images standard" id="aplusImages"></div>
</div>
</div>
</div>
</div>
<script>
class AmazonMockupGenerator {
constructor() {
this.uploadedImages = [];
this.csvData = {};
this.selectedHeroImage = null;
this.pinHero = false;
this.detectedEAN = '';
this.aplusLayout = 'standard';
this.initializeEventListeners();
}
initializeEventListeners() {
const imageUpload = document.getElementById('imageUpload');
const imageInput = document.getElementById('imageInput');
imageUpload.addEventListener('click', () => imageInput.click());
imageUpload.addEventListener('dragover', this.handleDragOver.bind(this));
imageUpload.addEventListener('dragleave', this.handleDragLeave.bind(this));
imageUpload.addEventListener('drop', this.handleImageDrop.bind(this));
imageInput.addEventListener('change', this.handleImageInput.bind(this));
const csvUpload = document.getElementById('csvUpload');
const csvInput = document.getElementById('csvInput');
csvUpload.addEventListener('click', () => csvInput.click());
csvUpload.addEventListener('dragover', this.handleDragOver.bind(this));
csvUpload.addEventListener('dragleave', this.handleDragLeave.bind(this));
csvUpload.addEventListener('drop', this.handleCsvDrop.bind(this));
csvInput.addEventListener('change', this.handleCsvInput.bind(this));
document.getElementById('exportBtn').addEventListener('click', this.exportMockup.bind(this));
document.getElementById('downloadAssetsBtn').addEventListener('click', this.downloadAssetsZip.bind(this));
document.getElementById('pinHeroToggle').addEventListener('change', (e) => { this.pinHero = e.target.checked; });
document.getElementById('eanInput').addEventListener('input', (e) => {
let digits = (e.target.value || '').replace(/\D/g, '');
if (digits.length > 13) digits = digits.slice(0, 13);
const padded = digits.padStart(13, '0');
this.detectedEAN = padded; e.target.value = padded;
});
document.getElementById('aplusLayoutSelect').addEventListener('change', (e) => {
this.aplusLayout = e.target.value; this.updateMockup();
});
document.getElementById('heroImage').onclick = () => document.getElementById('imageInput').click();
}
handleDragOver(e){ e.preventDefault(); e.currentTarget.classList.add('dragover'); }
handleDragLeave(e){ e.preventDefault(); e.currentTarget.classList.remove('dragover'); }
handleImageDrop(e){ e.preventDefault(); e.currentTarget.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files).filter(f => ['image/jpeg','image/jpg','image/png'].includes(f.type));
this.processImages(files);
}
handleImageInput(e){ const files = Array.from(e.target.files); this.processImages(files); }
handleCsvDrop(e){ e.preventDefault(); e.currentTarget.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files).filter(f => f.name.toLowerCase().endsWith('.csv'));
if (files.length) this.processCsv(files[0]);
}
handleCsvInput(e){ if (e.target.files.length) this.processCsv(e.target.files[0]); }
processImages(files){
files.forEach(file => {
const ok = ['image/jpeg','image/jpg','image/png'].includes(file.type);
if (!ok) return;
const reader = new FileReader();
reader.onload = (ev) => {
const imageData = { name: file.name, data: ev.target.result, file };
this.uploadedImages.push(imageData);
this.tryDetectEanFromName(file.name);
if (this.selectedHeroImage === null && this.uploadedImages.length > 0) this.selectedHeroImage = 0;
this.updateImagePreview(); this.updateMockup();
};
reader.readAsDataURL(file);
});
this.showStatus(`Added ${files.length} image(s)`, 'success');
}
tryDetectEanFromName(filename){ const nameOnly = filename.split('/').pop().split('\\').pop(); const m = nameOnly.match(/(\d{13}|\d{8})/); if (m && !this.detectedEAN){ this.detectedEAN = m[1]; const inp = document.getElementById('eanInput'); if (inp && !inp.value) inp.value = this.detectedEAN; } }
processCsv(file){ const reader = new FileReader(); reader.onload = (e)=>{ try { const txt = e.target.result; this.csvData = this.parseCsv(txt); this.updateCsvPreview(); this.updateMockup(); this.showStatus('CSV data loaded successfully','success'); } catch(err){ this.showStatus('Error parsing CSV: '+err.message,'error'); } }; reader.readAsText(file); }
parseCsv(csvText){
const lines = csvText.split(/\r?\n/); const data = {};
for (const raw of lines){ const line = raw.trim(); if (!line) continue;
const first = line.indexOf(','); if (first === -1) continue;
const second = line.indexOf(',', first+1); if (second === -1) continue;
const field = line.slice(0, first).trim(); let pos = second+1; let value='';
if (line[pos] === '"'){ pos++; let i=pos, end=-1; while(i<line.length){ if(line[i]==='"'){ if(line[i+1]==='"'){ i+=2; continue;} else { end=i; break; } } i++; } value = end!==-1? line.slice(pos,end): line.slice(pos); value = value.replace(/""/g,'"'); }
else { const third = line.indexOf(',', pos); value = third===-1? line.slice(pos): line.slice(pos, third); }
value = value.trim(); if (field) data[field]=value; }
return data;
}
updateImagePreview(){ const grid = document.getElementById('imageGrid'); if (!this.uploadedImages.length){ grid.innerHTML = '<div class="empty-state">No images uploaded yet</div>'; return; } grid.innerHTML=''; this.uploadedImages.forEach((img, idx)=>{ const el=document.createElement('img'); el.src=img.data; el.className='image-thumb'; el.title=img.name; el.addEventListener('click', ()=> this.promoteImageToHero(idx)); el.draggable=true; el.addEventListener('dragstart',(ev)=>this.handleThumbDragStart(ev,idx)); el.addEventListener('dragover',(ev)=>this.handleThumbDragOver(ev)); el.addEventListener('drop',(ev)=>this.handleThumbDrop(ev,idx)); if (this.selectedHeroImage===idx) el.classList.add('selected'); grid.appendChild(el); }); }
promoteImageToHero(index){ if (index>0 && index<this.uploadedImages.length){ if (!this.pinHero){ const [moved]=this.uploadedImages.splice(index,1); this.uploadedImages.unshift(moved); } } this.selectedHeroImage=0; this.updateImagePreview(); this.updateMockup(); }
handleThumbDragStart(ev,idx){ ev.dataTransfer.effectAllowed='move'; ev.dataTransfer.setData('text/plain', String(idx)); }
handleThumbDragOver(ev){ ev.preventDefault(); ev.dataTransfer.dropEffect='move'; }
handleThumbDrop(ev,targetIdx){ ev.preventDefault(); const s=ev.dataTransfer.getData('text/plain'); const src=parseInt(s,10); if (Number.isNaN(src)||src===targetIdx) return; const [moved]=this.uploadedImages.splice(src,1); this.uploadedImages.splice(targetIdx,0,moved); this.selectedHeroImage=0; this.updateImagePreview(); this.updateMockup(); }
updateCsvPreview(){ const box=document.getElementById('csvData'); if (!Object.keys(this.csvData).length){ box.innerHTML='<div class="empty-state">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; }
updateMockup(){
// Brand
const brand = this.csvData['DMI_Product_type'] || this.csvData.brand_name; if (brand) document.getElementById('brandName').textContent = brand;
// Title
const title = this.csvData['Amazon - Product Name (Long)'] || this.csvData.product_title; if (title) document.getElementById('productTitle').textContent = title;
// Bullets
const bulletsBox = document.getElementById('bullets'); bulletsBox.innerHTML='';
const bulletFields = ['Amazon - Bullet Features 1','Amazon - Bullet Features 2','Amazon - Bullet Features 3','Amazon - Bullet Features 4','Amazon - Bullet Features 5'];
let hasBullets=false; for (const key of bulletFields){ const val=this.csvData[key]; if (val){ const b=document.createElement('div'); b.className='bullet'; b.textContent=val; bulletsBox.appendChild(b); hasBullets=true; } }
if (!hasBullets){ for (let i=1;i<=5;i++){ const k=`bullet_${i}`; if (this.csvData[k]){ const b=document.createElement('div'); b.className='bullet'; b.textContent=this.csvData[k]; bulletsBox.appendChild(b);} } }
// Subtitle
const subtitleEl=document.getElementById('subtitle'); const benefits=this.csvData['Amazon Benefits']; if (benefits){ subtitleEl.style.display=''; subtitleEl.textContent=benefits; } else { subtitleEl.style.display='none'; subtitleEl.textContent=''; }
// Description
const desc=this.csvData['amazon_product_description_short'] || this.csvData.description; document.getElementById('description').textContent = desc || 'Product description will appear here...';
// Instructions
const instructionsEl=document.getElementById('instructions'); const iou=this.csvData['amazon_instruction_of_use']; if (iou){ instructionsEl.style.display=''; instructionsEl.textContent=iou; } else { instructionsEl.style.display='none'; instructionsEl.textContent=''; }
// Hero
const heroBox=document.getElementById('heroImage'); if (this.selectedHeroImage!==null && this.uploadedImages[this.selectedHeroImage]){ heroBox.innerHTML=''; const img=document.createElement('img'); img.src=this.uploadedImages[this.selectedHeroImage].data; img.style.width='100%'; img.style.height='100%'; img.style.objectFit='cover'; heroBox.appendChild(img);} else { heroBox.innerHTML='Drop your main product image here'; }
// Thumbnails
const thumbs=document.getElementById('thumbnailImages'); thumbs.innerHTML=''; this.uploadedImages.forEach((image, index)=>{ if (index===0) return; const t=document.createElement('img'); t.src=image.data; t.className='thumbnail'; t.addEventListener('click',()=>this.promoteImageToHero(index)); thumbs.appendChild(t); });
// A+ images (include hero as p1, then all others) with badges
const aplus=document.getElementById('aplusImages'); aplus.className = `aplus-images ${this.aplusLayout}`; aplus.innerHTML='';
this.uploadedImages.forEach((image,index)=>{ const tile=document.createElement('div'); tile.className='aplus-image'; const img=document.createElement('img'); img.src=image.data; tile.appendChild(img); const pos = index+1; const badge=document.createElement('span'); badge.className='order-badge'; badge.textContent = `p${pos}`; tile.appendChild(badge); aplus.appendChild(tile); });
// Enable buttons
document.getElementById('exportBtn').disabled = this.uploadedImages.length===0 && Object.keys(this.csvData).length===0;
document.getElementById('downloadAssetsBtn').disabled = this.uploadedImages.length===0;
}
showStatus(msg,type){ const div=document.getElementById('statusMessage'); div.innerHTML=`<div class="status-message status-${type}">${msg}</div>`; setTimeout(()=>{ div.innerHTML=''; },3000); }
async exportMockup(){ const mockup=document.getElementById('amazonMockup'); const btn=document.getElementById('exportBtn'); btn.disabled=true; btn.textContent='Exporting...'; try{
if (typeof html2canvas !== 'undefined'){
const canvas = await html2canvas(mockup, { backgroundColor:'#ffffff', scale:2 });
const ean = (this.detectedEAN || document.getElementById('eanInput').value || '').trim();
const fileName = `${ean ? ean + '_' : ''}ATF_AMZ.jpg`;
const dataUrl = canvas.toDataURL('image/jpeg', 0.92);
const link=document.createElement('a'); link.download=fileName; link.href=dataUrl; link.click();
} else { this.showStatus('Export requires html2canvas','error'); }
} catch(err){ this.showStatus('Export failed: '+err.message,'error'); } finally { btn.disabled=false; btn.textContent='Export as JPG'; } }
async downloadAssetsZip(){ try{
if (!this.uploadedImages.length) return; if (typeof JSZip==='undefined'){ this.showStatus('ZIP download requires JSZip','error'); return; }
const zip = new JSZip(); const folder = zip.folder('assets');
for (let i=0;i<this.uploadedImages.length;i++){ const img=this.uploadedImages[i]; const pos=i+1; const ext=(img.file && img.file.name.split('.').pop())||'jpg'; const base=(img.file && img.file.name.replace(/\.[^.]+$/, ''))||`image_${pos}`; const newName=`XXX_${base}_AMZ_p${pos}.${ext}`; let blob; if (img.file instanceof File || img.file instanceof Blob){ blob=img.file; } else { blob = await (await fetch(img.data)).blob(); } folder.file(newName, blob); }
const content = await zip.generateAsync({ type:'blob' }); const link=document.createElement('a'); link.href=URL.createObjectURL(content); const eanVal=(this.detectedEAN || document.getElementById('eanInput').value || '').trim(); link.download = `${eanVal ? eanVal + '_' : ''}AMZ.zip`; link.click(); URL.revokeObjectURL(link.href);
} catch(err){ this.showStatus('Failed to build ZIP: '+err.message,'error'); } }
}
// Boot
document.addEventListener('DOMContentLoaded', ()=>{ new AmazonMockupGenerator(); });
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
</body>
</html>

597
index_premium.html Executable file
View file

@ -0,0 +1,597 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Amazon PDP Premium Mockup Generator</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; }
.editor-section { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; margin-bottom: 30px; }
.editor-panel { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.editor-panel h3 { margin-bottom: 15px; color: #232f3e; }
.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); }
.image-thumb.selected { border-color: #ff9900; box-shadow: 0 0 0 2px rgba(255, 153, 0, 0.3); }
.csv-data { background: #f8f9fa; border-radius: 4px; padding: 15px; font-family: monospace; font-size: 14px; max-height: 200px; overflow-y: auto; }
.mockup-container { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
.mockup-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; gap: 10px; flex-wrap: wrap; }
.export-btn { background: #ff9900; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 600; transition: background 0.3s ease; }
.export-btn:disabled { background: #ccc; cursor: not-allowed; }
.amazon-mockup { background: white; border: 1px solid #ddd; border-radius: 4px; padding: 20px; font-family: Arial, sans-serif; max-width: 100%; }
.amazon-header { background: #232f3e; color: white; padding: 10px 20px; margin: -20px -20px 20px -20px; border-radius: 4px 4px 0 0; }
.product-section { display: grid; grid-template-columns: 420px 1fr; gap: 30px; margin-bottom: 30px; }
.hero-image { width: 100%; max-width: 400px; height: 400px; object-fit: cover; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa; display: flex; align-items: center; justify-content: center; color: #666; font-size: 14px; cursor: pointer; }
.thumbnail-images { display: flex; gap: 5px; flex-wrap: wrap; }
.thumbnail { width: 60px; height: 60px; object-fit: cover; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; }
.brand-name { color: #0066c0; font-size: 14px; margin-bottom: 5px; }
.product-title { font-size: 20px; font-weight: 400; line-height: 1.3; margin-bottom: 8px; color: #0f1111; }
.subtitle { font-size: 14px; color: #565959; margin-bottom: 12px; }
.bullets { margin-bottom: 20px; }
.bullet { margin-bottom: 8px; font-size: 14px; line-height: 1.4; }
.bullet::before { content: "• "; color: #ff9900; font-weight: bold; }
.description { font-size: 14px; line-height: 1.5; }
.instructions { margin-top: 10px; color: #565959; font-size: 13px; line-height: 1.45; }
.aplus-content { margin-top: 30px; border-top: 1px solid #ddd; padding-top: 20px; }
.aplus-images { display: grid; gap: 15px; }
.aplus-images.standard { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.aplus-images.large { grid-template-columns: repeat(3, 1fr); }
.aplus-image { width: 100%; aspect-ratio: 1/1; height: auto; border: 1px solid #ddd; border-radius: 4px; background: #ffffff; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; }
.aplus-image img { width: 100%; height: 100%; object-fit: contain; background: #ffffff; }
.order-badge { position: absolute; top: 6px; left: 6px; background: #232f3e; color: #fff; font-size: 12px; padding: 2px 6px; border-radius: 12px; font-weight: 600; }
.premium-section { margin-top: 30px; padding-top: 30px; border-top: 2px solid #ddd; }
.brand-story { margin-bottom: 30px; }
.brand-story-hero { width: 100%; height: 250px; background: #f0f0f0; border-radius: 8px; margin-bottom: 15px; display: flex; align-items: center; justify-content: center; color: #666; font-size: 16px; }
.brand-story-text { font-size: 15px; line-height: 1.6; color: #333; }
.comparison-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
.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-section { margin-bottom: 30px; }
.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; color: #333; }
.video-section { margin-bottom: 30px; }
.video-placeholder { width: 100%; height: 300px; background: #f0f0f0; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #666; font-size: 16px; }
.features-section { margin-bottom: 30px; }
.feature-item { margin-bottom: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px; display: flex; gap: 12px; align-items: flex-start; }
.feature-icon { width: 50px; height: 50px; background: #e0e0e0; border-radius: 8px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: #666; font-size: 20px; }
.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; color: #333; }
.ingredients-section { margin-bottom: 30px; }
.ingredients-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
.ingredient-item { padding: 15px; background: #f8f9fa; border-radius: 8px; text-align: center; }
.ingredient-name { font-weight: 600; font-size: 15px; margin-bottom: 6px; color: #232f3e; }
.ingredient-desc { font-size: 14px; line-height: 1.4; color: #333; }
.module-toggle { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.module-toggle input[type="checkbox"] { width: 18px; height: 18px; }
.module-toggle label { font-weight: 600; color: #232f3e; cursor: pointer; }
.module-content { display: none; }
.module-content.visible { display: block; }
.module-input { margin-bottom: 10px; }
.module-input textarea { width: 100%; min-height: 80px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: inherit; font-size: 14px; resize: vertical; }
.module-input input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: inherit; font-size: 14px; }
.module-input select { width: 100%; max-width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
.toolbar select { max-width: 230px; }
.comparison-editor { max-height: 300px; overflow-y: auto; }
.comparison-row { display: grid; grid-template-columns: 200px repeat(3, 1fr); gap: 8px; margin-bottom: 8px; align-items: center; }
.comparison-row input { padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
.comparison-row:first-child input { font-weight: 600; background: #f8f9fa; }
.faq-editor { max-height: 400px; overflow-y: auto; }
.faq-pair { margin-bottom: 15px; padding: 12px; background: #f8f9fa; border-radius: 6px; }
.faq-pair input { width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 6px; font-size: 13px; }
.faq-pair textarea { width: 100%; min-height: 60px; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; resize: vertical; }
.features-editor { max-height: 400px; overflow-y: auto; }
.feature-edit-item { margin-bottom: 15px; padding: 12px; background: #f8f9fa; border-radius: 6px; display: flex; gap: 8px; align-items: flex-start; }
.feature-edit-item input[type="text"] { padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
.feature-edit-item textarea { padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; resize: vertical; min-height: 60px; }
.ingredients-editor { max-height: 400px; overflow-y: auto; }
.ingredient-edit-item { margin-bottom: 15px; padding: 12px; background: #f8f9fa; border-radius: 6px; display: flex; gap: 8px; align-items: flex-start; }
.ingredient-edit-item input[type="text"] { padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; flex: 1; }
.ingredient-edit-item textarea { padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; resize: vertical; min-height: 60px; flex: 2; }
.add-btn { background: #ff9900; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; margin-top: 10px; }
.toolbar { display:flex; gap:10px; align-items:center; flex-wrap: wrap; }
@media (max-width: 768px) { .upload-section, .preview-section, .product-section, .editor-section { grid-template-columns: 1fr; } .container { padding: 10px; } }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Amazon PDP Premium Mockup Generator</h1>
<p>Upload assets, edit content, and generate professional mockups</p>
</div>
<!-- Uploaders moved to top -->
<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 moved here -->
<div class="preview-section">
<div class="image-preview">
<h3>Uploaded Images</h3>
<div class="image-grid" id="imageGrid"><div class="empty-state">No images uploaded yet</div></div>
</div>
<div class="csv-preview">
<h3>CSV Data Preview</h3>
<div class="csv-data" id="csvData"><div class="empty-state">No CSV data loaded yet</div></div>
</div>
</div>
<div class="editor-section">
<div class="editor-panel">
<h3>Premium Modules</h3>
<div class="module-toggle">
<input type="checkbox" id="brandStoryToggle">
<label for="brandStoryToggle">Brand Story</label>
<button id="applyBrandStoryBtn" class="add-btn" style="margin-left:auto;">Add to page</button>
</div>
<div class="module-content" id="brandStoryContent">
<div class="module-input">
<label>Brand Story Text</label>
<textarea id="brandStoryText" placeholder="Enter brand story narrative..."></textarea>
</div>
<div class="module-input">
<label>Hero Image (select from uploaded assets)</label>
<select id="brandStoryHeroSelect">
<option value="">None</option>
</select>
</div>
</div>
<div class="module-toggle">
<input type="checkbox" id="comparisonToggle">
<label for="comparisonToggle">Comparison Table</label>
<button id="applyComparisonBtn" class="add-btn" style="margin-left:auto;">Add to page</button>
</div>
<div class="module-content" id="comparisonContent">
<div class="comparison-editor" id="comparisonEditor">
<div class="comparison-row">
<input type="text" placeholder="Attribute">
<input type="text" placeholder="Product A">
<input type="text" placeholder="Product B">
<input type="text" placeholder="Product C">
</div>
<button id="addComparisonRow" class="add-btn">Add Row</button>
</div>
</div>
<div class="module-toggle">
<input type="checkbox" id="faqToggle">
<label for="faqToggle">FAQ</label>
<button id="applyFaqBtn" class="add-btn" style="margin-left:auto;">Add to page</button>
</div>
<div class="module-content" id="faqContent">
<div class="faq-editor" id="faqEditor">
<div class="faq-pair">
<input type="text" placeholder="Question">
<textarea placeholder="Answer"></textarea>
</div>
<button id="addFaqPair" class="add-btn">Add Q&A</button>
</div>
</div>
<div class="module-toggle">
<input type="checkbox" id="videoToggle">
<label for="videoToggle">Video</label>
<button id="applyVideoBtn" class="add-btn" style="margin-left:auto;">Add to page</button>
</div>
<div class="module-content" id="videoContent">
<div class="module-input">
<label>Video URL</label>
<input type="url" id="videoUrl" placeholder="https://example.com/video.mp4">
</div>
<div class="module-input">
<label>Video Thumbnail (select from uploaded assets)</label>
<select id="videoThumbnailSelect">
<option value="">None</option>
</select>
</div>
</div>
<div class="module-toggle">
<input type="checkbox" id="featuresToggle">
<label for="featuresToggle">Feature Callouts</label>
<button id="applyFeaturesBtn" class="add-btn" style="margin-left:auto;">Add to page</button>
</div>
<div class="module-content" id="featuresContent">
<div class="features-editor" id="featuresEditor">
<div class="feature-edit-item">
<input type="text" placeholder="Icon" style="width:60px;">
<input type="text" placeholder="Headline" style="flex:1;">
<textarea placeholder="Description" style="flex:2;"></textarea>
</div>
<button id="addFeatureItem" class="add-btn">Add Feature</button>
</div>
</div>
<div class="module-toggle">
<input type="checkbox" id="ingredientsToggle">
<label for="ingredientsToggle">Ingredients</label>
<button id="applyIngredientsBtn" class="add-btn" style="margin-left:auto;">Add to page</button>
</div>
<div class="module-content" id="ingredientsContent">
<div class="ingredients-editor" id="ingredientsEditor">
<div class="ingredient-edit-item">
<input type="text" placeholder="Ingredient Name" style="flex:1;">
<textarea placeholder="Description" style="flex:2;"></textarea>
</div>
<button id="addIngredientItem" class="add-btn">Add Ingredient</button>
</div>
</div>
</div>
<div class="mockup-container">
<div class="mockup-header">
<h3>Amazon Product Page Mockup</h3>
<div class="toolbar">
<label style="display:flex; align-items:center; gap:6px; font-size:14px; color:#232f3e;"><span>EAN</span><input id="eanInput" type="text" inputmode="numeric" pattern="\d{13}" maxlength="13" placeholder="13-digit EAN" style="height:30px; padding:4px 8px; border:1px solid #ddd; border-radius:4px; min-width:180px;" /></label>
<label style="display:flex; align-items:center; gap:6px; font-size:14px; color:#232f3e;"><input type="checkbox" id="pinHeroToggle"> Pin hero</label>
<label style="display:flex; align-items:center; gap:6px; font-size:14px; color:#232f3e;"><span>A+ layout</span><select id="aplusLayoutSelect" style="height:30px; padding:4px 8px; border:1px solid #ddd; border-radius:4px;"><option value="standard" selected>Standard (auto-fit)</option><option value="large">Large 3-up</option></select></label>
<button class="export-btn" id="downloadCsvBtn">Download CSV</button>
<button class="export-btn" id="downloadAssetsBtn" disabled>Download assets (ZIP)</button>
<button class="export-btn" id="exportBtn" disabled>Export as JPG</button>
</div>
</div>
<div class="amazon-mockup" id="amazonMockup">
<div class="amazon-header"><h2>Amazon.com</h2></div>
<div class="product-section">
<div class="product-images">
<div class="hero-image" id="heroImage">Drop your main product image here</div>
<div class="thumbnail-images" id="thumbnailImages"></div>
</div>
<div class="product-info">
<div class="brand-name" id="brandName">Brand Name</div>
<div class="product-title" id="productTitle">Product Title</div>
<div class="subtitle" id="subtitle" style="display:none;"></div>
<div class="bullets" id="bullets"></div>
<div class="description" id="description">Product description will appear here...</div>
<div class="instructions" id="instructions" style="display:none;"></div>
</div>
</div>
<div class="aplus-content">
<div class="aplus-images standard" id="aplusImages"></div>
</div>
<div class="premium-section" id="premiumSection">
<div class="brand-story" id="brandStory" style="display:none;">
<h4 style="margin-bottom:15px; color:#232f3e;">Brand Story</h4>
<button id="removeBrandStoryBtn" class="add-btn" style="position:absolute; top:10px; right:10px;">Remove</button>
<div class="brand-story-hero" id="brandStoryHero">Brand story hero image</div>
<div class="brand-story-text" id="brandStoryTextEl"></div>
</div>
<div id="comparisonTableContainer" style="display:none;">
<h4 style="margin-bottom:15px; color:#232f3e;">Compare with similar items</h4>
<button id="removeComparisonBtn" class="add-btn" style="position:absolute; top:10px; right:10px;">Remove</button>
<table class="comparison-table" id="comparisonTable"></table>
</div>
<div id="faqContainer" style="display:none;">
<h4 style="margin-bottom:15px; color:#232f3e;">Frequently Asked Questions</h4>
<button id="removeFaqBtn" class="add-btn" style="position:absolute; top:10px; right:10px;">Remove</button>
<div class="faq-section" id="faqSection"></div>
</div>
<div id="videoContainer" style="display:none;">
<h4 style="margin-bottom:15px; color:#232f3e;">Product Video</h4>
<button id="removeVideoBtn" class="add-btn" style="position:absolute; top:10px; right:10px;">Remove</button>
<div class="video-section" id="videoSection">
<div class="video-placeholder" id="videoPlaceholder">Video content will appear here</div>
</div>
</div>
<div id="featuresContainer" style="display:none;">
<h4 style="margin-bottom:15px; color:#232f3e;">Key Features</h4>
<button id="removeFeaturesBtn" class="add-btn" style="position:absolute; top:10px; right:10px;">Remove</button>
<div class="features-section" id="featuresSection"></div>
</div>
<div id="ingredientsContainer" style="display:none;">
<h4 style="margin-bottom:15px; color:#232f3e;">Key Ingredients & Technology</h4>
<button id="removeIngredientsBtn" class="add-btn" style="position:absolute; top:10px; right:10px;">Remove</button>
<div class="ingredients-section" id="ingredientsSection">
<div class="ingredients-grid" id="ingredientsGrid"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="statusMessage"></div>
</div>
<script>
class AmazonMockupGenerator {
constructor() {
this.uploadedImages = []; this.csvData = {}; this.selectedHeroImage = null; this.pinHero = false; this.detectedEAN = '';
this.brandStoryEnabled = false; this.comparisonEnabled = false; this.faqEnabled = false;
this.videoEnabled = false; this.featuresEnabled = false; this.ingredientsEnabled = false;
this.initializeEventListeners();
}
initializeEventListeners() {
const imageUpload = document.getElementById('imageUpload'), imageInput = document.getElementById('imageInput');
imageUpload.addEventListener('click', () => imageInput.click());
imageUpload.addEventListener('dragover', this.handleDragOver.bind(this));
imageUpload.addEventListener('dragleave', this.handleDragLeave.bind(this));
imageUpload.addEventListener('drop', this.handleImageDrop.bind(this));
imageInput.addEventListener('change', this.handleImageInput.bind(this));
const csvUpload = document.getElementById('csvUpload'), csvInput = document.getElementById('csvInput');
csvUpload.addEventListener('click', () => csvInput.click());
csvUpload.addEventListener('dragover', this.handleDragOver.bind(this));
csvUpload.addEventListener('dragleave', this.handleDragLeave.bind(this));
csvUpload.addEventListener('drop', this.handleCsvDrop.bind(this));
csvInput.addEventListener('change', this.handleCsvInput.bind(this));
document.getElementById('exportBtn').addEventListener('click', this.exportMockup.bind(this));
document.getElementById('downloadAssetsBtn').addEventListener('click', this.downloadAssetsZip.bind(this));
document.getElementById('pinHeroToggle').addEventListener('change', (e) => { this.pinHero = e.target.checked; });
document.getElementById('eanInput').addEventListener('input', (e) => { let d = (e.target.value || '').replace(/\D/g, ''); if (d.length > 13) d = d.slice(0, 13); const p = d.padStart(13, '0'); this.detectedEAN = p; e.target.value = p; });
document.getElementById('aplusLayoutSelect').addEventListener('change', (e) => { this.aplusLayout = e.target.value; this.updateMockup(); });
// default to standard layout explicitly
this.aplusLayout = 'standard';
document.getElementById('aplusLayoutSelect').value = 'standard';
// Download CSV button
document.getElementById('downloadCsvBtn').addEventListener('click', () => this.downloadCsv());
document.getElementById('heroImage').onclick = () => document.getElementById('imageInput').click();
document.getElementById('brandStoryToggle').addEventListener('change', (e) => { this.brandStoryEnabled = e.target.checked; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('comparisonToggle').addEventListener('change', (e) => { this.comparisonEnabled = e.target.checked; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('faqToggle').addEventListener('change', (e) => { this.faqEnabled = e.target.checked; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('videoToggle').addEventListener('change', (e) => { this.videoEnabled = e.target.checked; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('featuresToggle').addEventListener('change', (e) => { this.featuresEnabled = e.target.checked; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('ingredientsToggle').addEventListener('change', (e) => { this.ingredientsEnabled = e.target.checked; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('brandStoryText').addEventListener('input', () => this.updatePremiumModules());
document.getElementById('applyBrandStoryBtn').addEventListener('click', () => { this.brandStoryEnabled = true; this.updateTrayVisibility(); this.updatePremiumModules(); this.showStatus('Brand Story added to page','success'); });
document.getElementById('applyComparisonBtn').addEventListener('click', () => { this.comparisonEnabled = true; this.updateTrayVisibility(); this.updatePremiumModules(); this.showStatus('Comparison Table added to page','success'); });
document.getElementById('applyFaqBtn').addEventListener('click', () => { this.faqEnabled = true; this.updateTrayVisibility(); this.updatePremiumModules(); this.showStatus('FAQ added to page','success'); });
document.getElementById('applyVideoBtn').addEventListener('click', () => { this.videoEnabled = true; this.updateTrayVisibility(); this.updatePremiumModules(); this.showStatus('Video added to page','success'); });
document.getElementById('applyFeaturesBtn').addEventListener('click', () => { this.featuresEnabled = true; this.updateTrayVisibility(); this.updatePremiumModules(); this.showStatus('Feature Callouts added to page','success'); });
document.getElementById('applyIngredientsBtn').addEventListener('click', () => { this.ingredientsEnabled = true; this.updateTrayVisibility(); this.updatePremiumModules(); this.showStatus('Ingredients added to page','success'); });
document.getElementById('brandStoryHeroSelect').addEventListener('change', (e) => { this.updatePremiumModules(); });
document.getElementById('videoUrl').addEventListener('input', () => this.updatePremiumModules());
document.getElementById('videoThumbnailSelect').addEventListener('change', (e) => { this.updatePremiumModules(); });
document.getElementById('addComparisonRow').addEventListener('click', () => this.addComparisonRow());
document.getElementById('addFaqPair').addEventListener('click', () => this.addFaqPair());
document.getElementById('addFeatureItem').addEventListener('click', () => this.addFeatureItem());
document.getElementById('addIngredientItem').addEventListener('click', () => this.addIngredientItem());
document.getElementById('comparisonEditor').addEventListener('input', () => this.updatePremiumModules());
document.getElementById('faqEditor').addEventListener('input', () => this.updatePremiumModules());
document.getElementById('featuresEditor').addEventListener('input', () => this.updatePremiumModules());
document.getElementById('ingredientsEditor').addEventListener('input', () => this.updatePremiumModules());
// Remove buttons
document.getElementById('removeBrandStoryBtn').addEventListener('click', () => { this.brandStoryEnabled = false; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('removeComparisonBtn').addEventListener('click', () => { this.comparisonEnabled = false; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('removeFaqBtn').addEventListener('click', () => { this.faqEnabled = false; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('removeVideoBtn').addEventListener('click', () => { this.videoEnabled = false; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('removeFeaturesBtn').addEventListener('click', () => { this.featuresEnabled = false; this.updateTrayVisibility(); this.updatePremiumModules(); });
document.getElementById('removeIngredientsBtn').addEventListener('click', () => { this.ingredientsEnabled = false; this.updateTrayVisibility(); this.updatePremiumModules(); });
}
handleDragOver(e){ e.preventDefault(); e.currentTarget.classList.add('dragover'); }
handleDragLeave(e){ e.preventDefault(); e.currentTarget.classList.remove('dragover'); }
handleImageDrop(e){ e.preventDefault(); e.currentTarget.classList.remove('dragover'); const files = Array.from(e.dataTransfer.files).filter(f => ['image/jpeg','image/jpg','image/png'].includes(f.type)); this.processImages(files); }
handleImageInput(e){ const files = Array.from(e.target.files); this.processImages(files); }
handleCsvDrop(e){ e.preventDefault(); e.currentTarget.classList.remove('dragover'); const files = Array.from(e.dataTransfer.files).filter(f => f.name.toLowerCase().endsWith('.csv')); if (files.length) this.processCsv(files[0]); }
handleCsvInput(e){ if (e.target.files.length) this.processCsv(e.target.files[0]); }
processImages(files){ files.forEach(file => { const ok = ['image/jpeg','image/jpg','image/png'].includes(file.type); if (!ok) return; const reader = new FileReader(); reader.onload = (ev) => { const imageData = { name: file.name, data: ev.target.result, file }; this.uploadedImages.push(imageData); this.tryDetectEanFromName(file.name); if (this.selectedHeroImage === null && this.uploadedImages.length > 0) this.selectedHeroImage = 0; this.updateImagePreview(); this.updateMockup(); }; reader.readAsDataURL(file); }); this.showStatus(`Added ${files.length} image(s)`, 'success'); }
tryDetectEanFromName(filename){ const n = filename.split('/').pop().split('\\').pop(); const m = n.match(/(\d{13}|\d{8})/); if (m && !this.detectedEAN){ this.detectedEAN = m[1]; const inp = document.getElementById('eanInput'); if (inp && !inp.value) inp.value = this.detectedEAN; } }
processCsv(file){ const reader = new FileReader(); reader.onload = (e)=>{ try { const txt = e.target.result; this.csvData = this.parseCsv(txt); this.updateCsvPreview(); this.updateMockup(); this.showStatus('CSV data loaded successfully','success'); } catch(err){ this.showStatus('Error parsing CSV: '+err.message,'error'); } }; reader.readAsText(file); }
parseCsv(csvText){ const lines = csvText.split(/\r?\n/); const data = {}; for (const raw of lines){ const line = raw.trim(); if (!line) continue; const first = line.indexOf(','); if (first === -1) continue; const second = line.indexOf(',', first+1); if (second === -1) continue; const field = line.slice(0, first).trim(); let pos = second+1; let value=''; if (line[pos] === '"'){ pos++; let i=pos, end=-1; while(i<line.length){ if(line[i]==='"'){ if(line[i+1]==='"'){ i+=2; continue;} else { end=i; break; } } i++; } value = end!==-1? line.slice(pos,end): line.slice(pos); value = value.replace(/""/g,'"'); } else { const third = line.indexOf(',', pos); value = third===-1? line.slice(pos): line.slice(pos, third); } value = value.trim(); if (field) data[field]=value; } return data; }
updateImagePreview(){ const grid = document.getElementById('imageGrid'); if (!this.uploadedImages.length){ grid.innerHTML = '<div class="empty-state">No images uploaded yet</div>'; return; } grid.innerHTML=''; this.uploadedImages.forEach((img, idx)=>{ const el=document.createElement('img'); el.src=img.data; el.className='image-thumb'; el.title=img.name; el.addEventListener('click', ()=> this.promoteImageToHero(idx)); el.draggable=true; el.addEventListener('dragstart',(ev)=>this.handleThumbDragStart(ev,idx)); el.addEventListener('dragover',(ev)=>this.handleThumbDragOver(ev)); el.addEventListener('drop',(ev)=>this.handleThumbDrop(ev,idx)); if (this.selectedHeroImage===idx) el.classList.add('selected'); grid.appendChild(el); }); }
promoteImageToHero(index){ if (index>0 && index<this.uploadedImages.length){ if (!this.pinHero){ const [moved]=this.uploadedImages.splice(index,1); this.uploadedImages.unshift(moved); } } this.selectedHeroImage=0; this.updateImagePreview(); this.updateMockup(); }
handleThumbDragStart(ev,idx){ ev.dataTransfer.effectAllowed='move'; ev.dataTransfer.setData('text/plain', String(idx)); }
handleThumbDragOver(ev){ ev.preventDefault(); ev.dataTransfer.dropEffect='move'; }
handleThumbDrop(ev,targetIdx){ ev.preventDefault(); const s=ev.dataTransfer.getData('text/plain'); const src=parseInt(s,10); if (Number.isNaN(src)||src===targetIdx) return; const [moved]=this.uploadedImages.splice(src,1); this.uploadedImages.splice(targetIdx,0,moved); this.selectedHeroImage=0; this.updateImagePreview(); this.updateMockup(); }
updateCsvPreview(){ const box=document.getElementById('csvData'); if (!Object.keys(this.csvData).length){ box.innerHTML='<div class="empty-state">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; }
updateMockup(){
const brand = this.csvData['DMI_Product_type'] || this.csvData.brand_name; if (brand) document.getElementById('brandName').textContent = brand;
const title = this.csvData['Amazon - Product Name (Long)'] || this.csvData.product_title; if (title) document.getElementById('productTitle').textContent = title;
const bulletsBox = document.getElementById('bullets'); bulletsBox.innerHTML=''; const bulletFields = ['Amazon - Bullet Features 1','Amazon - Bullet Features 2','Amazon - Bullet Features 3','Amazon - Bullet Features 4','Amazon - Bullet Features 5']; let hasBullets=false; for (const key of bulletFields){ const val=this.csvData[key]; if (val){ const b=document.createElement('div'); b.className='bullet'; b.textContent=val; bulletsBox.appendChild(b); hasBullets=true; } } if (!hasBullets){ for (let i=1;i<=5;i++){ const k=`bullet_${i}`; if (this.csvData[k]){ const b=document.createElement('div'); b.className='bullet'; b.textContent=this.csvData[k]; bulletsBox.appendChild(b);} } }
const subtitleEl=document.getElementById('subtitle'); const benefits=this.csvData['Amazon Benefits']; if (benefits){ subtitleEl.style.display=''; subtitleEl.textContent=benefits; } else { subtitleEl.style.display='none'; subtitleEl.textContent=''; }
const desc=this.csvData['amazon_product_description_short'] || this.csvData.description; document.getElementById('description').textContent = desc || 'Product description will appear here...';
const instructionsEl=document.getElementById('instructions'); const iou=this.csvData['amazon_instruction_of_use']; if (iou){ instructionsEl.style.display=''; instructionsEl.textContent=iou; } else { instructionsEl.style.display='none'; instructionsEl.textContent=''; }
const heroBox=document.getElementById('heroImage'); if (this.selectedHeroImage!==null && this.uploadedImages[this.selectedHeroImage]){ heroBox.innerHTML=''; const img=document.createElement('img'); img.src=this.uploadedImages[this.selectedHeroImage].data; img.style.width='100%'; img.style.height='100%'; img.style.objectFit='cover'; heroBox.appendChild(img);} else { heroBox.innerHTML='Drop your main product image here'; }
const thumbs=document.getElementById('thumbnailImages'); thumbs.innerHTML=''; this.uploadedImages.forEach((image, index)=>{ if (index===0) return; const t=document.createElement('img'); t.src=image.data; t.className='thumbnail'; t.addEventListener('click',()=>this.promoteImageToHero(index)); thumbs.appendChild(t); });
const aplus=document.getElementById('aplusImages'); aplus.className = `aplus-images ${this.aplusLayout}`; aplus.innerHTML=''; this.uploadedImages.forEach((image,index)=>{ const tile=document.createElement('div'); tile.className='aplus-image'; const img=document.createElement('img'); img.src=image.data; tile.appendChild(img); const pos = index+1; const badge=document.createElement('span'); badge.className='order-badge'; badge.textContent = `p${pos}`; tile.appendChild(badge); aplus.appendChild(tile); });
this.updatePremiumModules();
document.getElementById('exportBtn').disabled = this.uploadedImages.length===0 && Object.keys(this.csvData).length===0; document.getElementById('downloadAssetsBtn').disabled = this.uploadedImages.length===0;
}
updateTrayVisibility(){
document.getElementById('brandStoryContent').classList.toggle('visible', this.brandStoryEnabled);
document.getElementById('comparisonContent').classList.toggle('visible', this.comparisonEnabled);
document.getElementById('faqContent').classList.toggle('visible', this.faqEnabled);
document.getElementById('videoContent').classList.toggle('visible', this.videoEnabled);
document.getElementById('featuresContent').classList.toggle('visible', this.featuresEnabled);
document.getElementById('ingredientsContent').classList.toggle('visible', this.ingredientsEnabled);
const heroSelect = document.getElementById('brandStoryHeroSelect');
const videoThumbSelect = document.getElementById('videoThumbnailSelect');
const prevHero = heroSelect.value;
const prevThumb = videoThumbSelect.value;
heroSelect.innerHTML = '<option value="">None</option>';
videoThumbSelect.innerHTML = '<option value="">None</option>';
this.uploadedImages.forEach((img, idx) => {
const opt1 = document.createElement('option'); opt1.value = String(idx); opt1.textContent = img.name; heroSelect.appendChild(opt1);
const opt2 = document.createElement('option'); opt2.value = String(idx); opt2.textContent = img.name; videoThumbSelect.appendChild(opt2);
});
// restore previous selections if still valid
if (prevHero !== '' && Number(prevHero) < this.uploadedImages.length) heroSelect.value = prevHero; else heroSelect.value = '';
if (prevThumb !== '' && Number(prevThumb) < this.uploadedImages.length) videoThumbSelect.value = prevThumb; else videoThumbSelect.value = '';
}
updatePremiumModules(){
// Update tray visibility first
this.updateTrayVisibility();
// Brand Story
const brandStoryEl = document.getElementById('brandStory'); const brandStoryTextEl = document.getElementById('brandStoryText');
if (this.brandStoryEnabled){
brandStoryEl.style.display='block';
document.getElementById('brandStoryTextEl').textContent = brandStoryTextEl.value.trim() || 'Brand story text will appear here...';
const sel = heroSelect.value;
const heroIdx = sel === '' ? -1 : parseInt(sel, 10);
if (!Number.isNaN(heroIdx) && heroIdx >= 0 && this.uploadedImages[heroIdx]){
document.getElementById('brandStoryHero').innerHTML = '<img src="' + this.uploadedImages[heroIdx].data + '" style="width:100%; height:250px; object-fit:cover;">';
} else {
document.getElementById('brandStoryHero').innerHTML = 'Brand story hero image';
}
} else {
brandStoryEl.style.display='none';
}
// Comparison
const comparisonEl = document.getElementById('comparisonTableContainer'); const comparisonTable = document.getElementById('comparisonTable');
if (this.comparisonEnabled){ comparisonEl.style.display='block'; comparisonTable.innerHTML=''; const rows = document.querySelectorAll('.comparison-row'); if (rows.length > 0){ const tr = document.createElement('tr'); const firstRow = rows[0]; const inputs = firstRow.querySelectorAll('input'); inputs.forEach(inp => { const th = document.createElement('th'); th.textContent = inp.value || inp.placeholder; tr.appendChild(th); }); comparisonTable.appendChild(tr); for (let i = 1; i < rows.length; i++){ const r = document.createElement('tr'); const ins = rows[i].querySelectorAll('input'); ins.forEach(inp => { const td = document.createElement('td'); td.textContent = inp.value || inp.placeholder; r.appendChild(td); }); comparisonTable.appendChild(r); } } } else { comparisonEl.style.display='none'; }
// FAQ
const faqEl = document.getElementById('faqContainer'); const faqSec = document.getElementById('faqSection'); faqSec.innerHTML=''; let hasFaq=false; const pairs = document.querySelectorAll('.faq-pair'); pairs.forEach(pair => { const qInp = pair.querySelector('input'); const aTxt = pair.querySelector('textarea'); if (qInp.value.trim() && aTxt.value.trim()){ const item = document.createElement('div'); item.className='faq-item'; item.innerHTML = '<div class="faq-question">' + qInp.value.trim() + '</div><div class="faq-answer">' + aTxt.value.trim() + '</div>'; faqSec.appendChild(item); hasFaq=true; } }); faqEl.style.display = hasFaq ? 'block' : 'none';
// Video
const videoEl = document.getElementById('videoContainer'); const videoUrl = document.getElementById('videoUrl').value.trim(); const videoThumbIdx = parseInt(document.getElementById('videoThumbnailSelect').value) || -1;
if (this.videoEnabled){ videoEl.style.display='block'; let content = videoUrl ? `Video: ${videoUrl}` : 'Video content will appear here...'; if (videoThumbIdx >= 0 && this.uploadedImages[videoThumbIdx]){ content = '<img src="' + this.uploadedImages[videoThumbIdx].data + '" style="width:100%; height:300px; object-fit:cover;">'; } document.getElementById('videoPlaceholder').innerHTML = content; } else { videoEl.style.display='none'; }
// Features
const featuresEl = document.getElementById('featuresContainer'); const featuresSec = document.getElementById('featuresSection'); featuresSec.innerHTML=''; let hasFeatures=false; const featureItems = document.querySelectorAll('.feature-edit-item'); featureItems.forEach(item => { const iconInp = item.querySelector('input[type="text"]'); const headlineInp = item.querySelectorAll('input[type="text"]')[1]; const textArea = item.querySelector('textarea'); if (headlineInp.value.trim() && textArea.value.trim()){ const feature = document.createElement('div'); feature.className='feature-item'; feature.innerHTML = '<div class="feature-icon">' + (iconInp.value.trim() || '⭐') + '</div><div class="feature-content"><div class="feature-headline">' + headlineInp.value.trim() + '</div><div class="feature-text">' + textArea.value.trim() + '</div></div>'; featuresSec.appendChild(feature); hasFeatures=true; } else if (headlineInp.value.trim() || textArea.value.trim() || iconInp.value.trim()){ const feature = document.createElement('div'); feature.className='feature-item'; feature.innerHTML = '<div class="feature-icon">' + (iconInp.value.trim() || '⭐') + '</div><div class="feature-content"><div class="feature-headline">' + (headlineInp.value.trim() || 'Feature headline...') + '</div><div class="feature-text">' + (textArea.value.trim() || 'Feature description will appear here...') + '</div></div>'; featuresSec.appendChild(feature); hasFeatures=true; } }); featuresEl.style.display = this.featuresEnabled ? 'block' : 'none';
// Ingredients
const ingredientsEl = document.getElementById('ingredientsContainer'); const ingredientsGrid = document.getElementById('ingredientsGrid'); ingredientsGrid.innerHTML=''; let hasIngredients=false; const ingredientItems = document.querySelectorAll('.ingredient-edit-item'); ingredientItems.forEach(item => { const nameInp = item.querySelector('input[type="text"]'); const descArea = item.querySelector('textarea'); if (nameInp.value.trim() && descArea.value.trim()){ const ingredient = document.createElement('div'); ingredient.className='ingredient-item'; ingredient.innerHTML = '<div class="ingredient-name">' + nameInp.value.trim() + '</div><div class="ingredient-desc">' + descArea.value.trim() + '</div>'; ingredientsGrid.appendChild(ingredient); hasIngredients=true; } else if (nameInp.value.trim() || descArea.value.trim()){ const ingredient = document.createElement('div'); ingredient.className='ingredient-item'; ingredient.innerHTML = '<div class="ingredient-name">' + (nameInp.value.trim() || 'Ingredient name...') + '</div><div class="ingredient-desc">' + (descArea.value.trim() || 'Ingredient description will appear here...') + '</div>'; ingredientsGrid.appendChild(ingredient); hasIngredients=true; } }); ingredientsEl.style.display = this.ingredientsEnabled ? 'block' : 'none';
}
addComparisonRow(){ const editor = document.getElementById('comparisonEditor'); const newRow = document.createElement('div'); newRow.className = 'comparison-row'; newRow.innerHTML = '<input type="text" placeholder="Attribute"><input type="text" placeholder="Value A"><input type="text" placeholder="Value B"><input type="text" placeholder="Value C">'; editor.appendChild(newRow); this.updatePremiumModules(); }
addFaqPair(){ const editor = document.getElementById('faqEditor'); const newPair = document.createElement('div'); newPair.className = 'faq-pair'; newPair.innerHTML = '<input type="text" placeholder="Question"><textarea placeholder="Answer"></textarea>'; editor.appendChild(newPair); this.updatePremiumModules(); }
addFeatureItem(){ const editor = document.getElementById('featuresEditor'); const newItem = document.createElement('div'); newItem.className = 'feature-edit-item'; newItem.innerHTML = '<input type="text" placeholder="Icon" value="⭐" style="width:60px;"><input type="text" placeholder="Headline" style="flex:1;"><textarea placeholder="Description" style="flex:2;"></textarea>'; editor.appendChild(newItem); this.updatePremiumModules(); }
addIngredientItem(){ const editor = document.getElementById('ingredientsEditor'); const newItem = document.createElement('div'); newItem.className = 'ingredient-edit-item'; newItem.innerHTML = '<input type="text" placeholder="Ingredient Name" style="flex:1;"><textarea placeholder="Description" style="flex:2;"></textarea>'; editor.appendChild(newItem); this.updatePremiumModules(); }
showStatus(msg,type){ const div=document.getElementById('statusMessage'); div.innerHTML=`<div style="padding:10px; border-radius:4px; margin-bottom:15px; font-size:14px; background:${type==='success'?'#d4edda':'#f8d7da'}; color:${type==='success'?'#155724':'#721c24'}; border:1px solid ${type==='success'?'#c3e6cb':'#f5c6cb'};">${msg}</div>`; setTimeout(()=>{ div.innerHTML=''; },3000); }
downloadCsv(){
// Build rows as [field, , value]
const rows = [];
const push = (k, v) => { if (v && String(v).trim()) rows.push([k, '', String(v).trim()]); };
// Base fields
push('DMI_Product_type', document.getElementById('brandName').textContent);
push('Amazon - Product Name (Long)', document.getElementById('productTitle').textContent);
push('Amazon Benefits', document.getElementById('subtitle').textContent);
// Bullets from DOM
Array.from(document.getElementById('bullets').children).forEach((el, i) => push(`Amazon - Bullet Features ${i+1}`, el.textContent));
push('amazon_product_description_short', document.getElementById('description').textContent);
push('amazon_instruction_of_use', document.getElementById('instructions').style.display !== 'none' ? document.getElementById('instructions').textContent : '');
// Brand Story
if (this.brandStoryEnabled){
const txt = document.getElementById('brandStoryText').value;
push('premium_brand_story_text', txt);
}
// Comparison: header row then attribute rows
if (this.comparisonEnabled){
const rowsEls = document.querySelectorAll('.comparison-row');
if (rowsEls.length){
const hdr = rowsEls[0].querySelectorAll('input');
push('premium_comparison_sku_1_name', hdr[1]?.value || '');
push('premium_comparison_sku_2_name', hdr[2]?.value || '');
push('premium_comparison_sku_3_name', hdr[3]?.value || '');
for (let i=1;i<rowsEls.length;i++){
const ins = rowsEls[i].querySelectorAll('input');
const idx = i; // 1-based attribute index
push(`premium_comparison_attr_${idx}`, ins[0]?.value || '');
push(`premium_comparison_sku_1_attr_${idx}`, ins[1]?.value || '');
push(`premium_comparison_sku_2_attr_${idx}`, ins[2]?.value || '');
push(`premium_comparison_sku_3_attr_${idx}`, ins[3]?.value || '');
}
}
}
// FAQ
if (this.faqEnabled){
const pairs = document.querySelectorAll('.faq-pair');
let n=1; pairs.forEach(p=>{ const q=p.querySelector('input')?.value||''; const a=p.querySelector('textarea')?.value||''; push(`premium_faq_question_${n}`, q); push(`premium_faq_answer_${n}`, a); n++; });
}
// Video
if (this.videoEnabled){
push('premium_video_url', document.getElementById('videoUrl').value || '');
push('premium_video_thumbnail_index', document.getElementById('videoThumbnailSelect').value || '');
}
// Features
if (this.featuresEnabled){
const items = document.querySelectorAll('.feature-edit-item');
let n=1; items.forEach(it=>{ const icon=it.querySelector('input[type="text"]')?.value||''; const head=it.querySelectorAll('input[type="text"]')[1]?.value||''; const txt=it.querySelector('textarea')?.value||''; push(`premium_feature_${n}_icon`, icon); push(`premium_feature_${n}_headline`, head); push(`premium_feature_${n}_text`, txt); n++; });
}
// Ingredients
if (this.ingredientsEnabled){
const items = document.querySelectorAll('.ingredient-edit-item');
let n=1; items.forEach(it=>{ const name=it.querySelector('input[type="text"]')?.value||''; const desc=it.querySelector('textarea')?.value||''; push(`premium_ingredient_${n}_name`, name); push(`premium_ingredient_${n}_desc`, desc); n++; });
}
// Build CSV text
let csv = '';
rows.forEach(([k, , v])=>{
const safe = '"' + String(v).replace(/"/g,'""') + '"';
csv += `${k},,${safe}\n`;
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'premium_structure.csv';
link.click();
URL.revokeObjectURL(link.href);
}
async exportMockup(){ const mockup=document.getElementById('amazonMockup'); const btn=document.getElementById('exportBtn'); btn.disabled=true; btn.textContent='Exporting...'; try{ if (typeof html2canvas !== 'undefined'){ const canvas = await html2canvas(mockup, { backgroundColor:'#ffffff', scale:2 }); const ean = (this.detectedEAN || document.getElementById('eanInput').value || '').trim(); const fileName = `${ean ? ean + '_' : ''}ATF_AMZ.jpg`; const dataUrl = canvas.toDataURL('image/jpeg', 0.92); const link=document.createElement('a'); link.download=fileName; link.href=dataUrl; link.click(); } else { this.showStatus('Export requires html2canvas','error'); } } catch(err){ this.showStatus('Export failed: '+err.message,'error'); } finally { btn.disabled=false; btn.textContent='Export as JPG'; } }
async downloadAssetsZip(){ try{ if (!this.uploadedImages.length) return; if (typeof JSZip==='undefined'){ this.showStatus('ZIP download requires JSZip','error'); return; } const zip = new JSZip(); const folder = zip.folder('assets'); for (let i=0;i<this.uploadedImages.length;i++){ const img=this.uploadedImages[i]; const pos=i+1; const ext=(img.file && img.file.name.split('.').pop())||'jpg'; const base=(img.file && img.file.name.replace(/\.[^.]+$/, ''))||`image_${pos}`; const newName=`XXX_${base}_AMZ_p${pos}.${ext}`; let blob; if (img.file instanceof File || img.file instanceof Blob){ blob=img.file; } else { blob = await (await fetch(img.data)).blob(); } folder.file(newName, blob); } const content = await zip.generateAsync({ type:'blob' }); const link=document.createElement('a'); link.href=URL.createObjectURL(content); const eanVal=(this.detectedEAN || document.getElementById('eanInput').value || '').trim(); link.download = `${eanVal ? eanVal + '_' : ''}AMZ.zip`; link.click(); URL.revokeObjectURL(link.href); } catch(err){ this.showStatus('Failed to build ZIP: '+err.message,'error'); } }
}
document.addEventListener('DOMContentLoaded', ()=>{ new AmazonMockupGenerator(); });
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
</body>
</html>

12
sample.csv Executable file
View file

@ -0,0 +1,12 @@
field,content
brand_name,L'Oréal Paris
product_title,Advanced Hair Serum - Keratin Infused 100ml
bullet_1,Professional-grade formula with keratin complex for stronger hair
bullet_2,Suitable for all hair types and textures
bullet_3,Reduces frizz and adds natural shine
bullet_4,Dermatologically tested and approved
bullet_5,Cruelty-free and vegan friendly
description,Transform your hair care routine with our advanced serum that combines the power of keratin with nourishing ingredients. This professional-grade formula penetrates deep into the hair shaft to repair damage, reduce frizz, and add natural shine. Perfect for daily use, this serum is suitable for all hair types and leaves your hair feeling silky smooth and manageable.
price,$24.99
rating,4.5
review_count,1234
1 field,content
2 brand_name,L'Oréal Paris
3 product_title,Advanced Hair Serum - Keratin Infused 100ml
4 bullet_1,Professional-grade formula with keratin complex for stronger hair
5 bullet_2,Suitable for all hair types and textures
6 bullet_3,Reduces frizz and adds natural shine
7 bullet_4,Dermatologically tested and approved
8 bullet_5,Cruelty-free and vegan friendly
9 description,Transform your hair care routine with our advanced serum that combines the power of keratin with nourishing ingredients. This professional-grade formula penetrates deep into the hair shaft to repair damage, reduce frizz, and add natural shine. Perfect for daily use, this serum is suitable for all hair types and leaves your hair feeling silky smooth and manageable.
10 price,$24.99
11 rating,4.5
12 review_count,1234

44
sample_premium.csv Executable file
View file

@ -0,0 +1,44 @@
DMI_Product_type,,L'Oréal Professionnel
Amazon - Product Name (Long),,L'Oréal Professionnel Curl Expression Leave-in hidratante intensivo de longa duração SERIE EXPERT 200ml Para todos os tipos de caracóis e ondas
Amazon Benefits,,Hidrata profundamente
Amazon - Bullet Features 1,,Hidratação de longa duração
Amazon - Bullet Features 2,,O leave-in hidratante de longa duração Curl Expression para ondas e caracóis
Amazon - Bullet Features 3,,Serie Expert Curl Expression, Expressa os teus caracóis de forma pro.
Amazon - Bullet Features 4,,Quando necessário
Amazon - Bullet Features 5,,Serie Expert
amazon_product_description_short,,[CUIDE OS SEUS CARACÓIS COMO UM PRO]: O leave-in hidratante de longa duração Curl Expression protege o cabelo até 230°C/450F com uma fórmula untuosa duradoura sem adicionar peso ao cabelo, de forma profissional. [ATIVOS ALTAMENTE CONCENTRADOS]: Com glicerina à base de plantas e Ureia HCom Glicerina à base de plantas, Ureia H e semente de hibisco para uns caracóis e ondas fortes e hidratadas. [EXPERIÊNCIA ÚNICA]: Esta fórmula sem parabenos deixa os caracóis e ondas intensamente hidratados e fortalecidos. [CODESENVOLVIDO COM ESPECIALISTAS EM CARACÓIS]: Com 4 especialistas internacionais em ondas e caracóis: Titus Magida da África do Sul(@call.me.titus), Vivi Siqueira do Brazil (@vivi_siqueira), Derick Monroe dos Estados Unidos (@derickmonroe) e Elodie Euston da França (@elodieeuston).
amazon_instruction_of_use,,Com o cabelo húmido, aplicar com os dedos desde os comprimentos até às pontas.
premium_brand_story_text,,L'Oréal Professionnel has been at the forefront of hair care innovation for over 100 years. Our Curl Expression line represents the culmination of decades of research into curly hair needs, developed in partnership with curl specialists worldwide. This leave-in treatment combines cutting-edge science with natural ingredients to deliver professional results at home.
premium_comparison_sku_1_name,,Curl Expression 200ml
premium_comparison_sku_2_name,,Curl Expression 100ml
premium_comparison_sku_3_name,,Curl Expression 300ml
premium_comparison_attr_1,,Size
premium_comparison_sku_1_attr_1,,200ml
premium_comparison_sku_2_attr_1,,100ml
premium_comparison_sku_3_attr_1,,300ml
premium_comparison_attr_2,,Price
premium_comparison_sku_1_attr_2,,$24.99
premium_comparison_sku_2_attr_2,,$14.99
premium_comparison_sku_3_attr_2,,$34.99
premium_comparison_attr_3,,Heat Protection
premium_comparison_sku_1_attr_3,,Up to 230°C
premium_comparison_sku_2_attr_3,,Up to 230°C
premium_comparison_sku_3_attr_3,,Up to 230°C
premium_comparison_attr_4,,Hair Types
premium_comparison_sku_1_attr_4,,All curl types
premium_comparison_sku_2_attr_4,,All curl types
premium_comparison_sku_3_attr_4,,All curl types
premium_comparison_attr_5,,Key Ingredient
premium_comparison_sku_1_attr_5,,3% Plant Glycerin
premium_comparison_sku_2_attr_5,,3% Plant Glycerin
premium_comparison_sku_3_attr_5,,3% Plant Glycerin
premium_faq_question_1,,How often should I use this product?
premium_faq_answer_1,,Use as needed, typically 2-3 times per week or whenever your curls need extra hydration and definition.
premium_faq_question_2,,Can I use this on color-treated hair?
premium_faq_answer_2,,Yes, this product is safe for color-treated hair and won't strip or fade your color.
premium_faq_question_3,,What's the difference between this and regular conditioner?
premium_faq_answer_3,,This is a leave-in treatment that provides continuous hydration and heat protection, while conditioner is rinsed out and provides temporary conditioning.
premium_faq_question_4,,Is this product suitable for fine hair?
premium_faq_answer_4,,Yes, the lightweight formula won't weigh down fine hair while still providing the hydration and definition your curls need.
premium_faq_question_5,,How long does one bottle last?
premium_faq_answer_5,,A 200ml bottle typically lasts 2-3 months with regular use, depending on hair length and thickness.
1 DMI_Product_type,,L'Oréal Professionnel
2 Amazon - Product Name (Long),,L'Oréal Professionnel Curl Expression Leave-in hidratante intensivo de longa duração SERIE EXPERT 200ml Para todos os tipos de caracóis e ondas
3 Amazon Benefits,,Hidrata profundamente
4 Amazon - Bullet Features 1,,Hidratação de longa duração
5 Amazon - Bullet Features 2,,O leave-in hidratante de longa duração Curl Expression para ondas e caracóis
6 Amazon - Bullet Features 3,,Serie Expert Curl Expression, Expressa os teus caracóis de forma pro.
7 Amazon - Bullet Features 4,,Quando necessário
8 Amazon - Bullet Features 5,,Serie Expert
9 amazon_product_description_short,,[CUIDE OS SEUS CARACÓIS COMO UM PRO]: O leave-in hidratante de longa duração Curl Expression protege o cabelo até 230°C/450F com uma fórmula untuosa duradoura sem adicionar peso ao cabelo, de forma profissional. [ATIVOS ALTAMENTE CONCENTRADOS]: Com glicerina à base de plantas e Ureia HCom Glicerina à base de plantas, Ureia H e semente de hibisco para uns caracóis e ondas fortes e hidratadas. [EXPERIÊNCIA ÚNICA]: Esta fórmula sem parabenos deixa os caracóis e ondas intensamente hidratados e fortalecidos. [CODESENVOLVIDO COM ESPECIALISTAS EM CARACÓIS]: Com 4 especialistas internacionais em ondas e caracóis: Titus Magida da África do Sul(@call.me.titus), Vivi Siqueira do Brazil (@vivi_siqueira), Derick Monroe dos Estados Unidos (@derickmonroe) e Elodie Euston da França (@elodieeuston).
10 amazon_instruction_of_use,,Com o cabelo húmido, aplicar com os dedos desde os comprimentos até às pontas.
11 premium_brand_story_text,,L'Oréal Professionnel has been at the forefront of hair care innovation for over 100 years. Our Curl Expression line represents the culmination of decades of research into curly hair needs, developed in partnership with curl specialists worldwide. This leave-in treatment combines cutting-edge science with natural ingredients to deliver professional results at home.
12 premium_comparison_sku_1_name,,Curl Expression 200ml
13 premium_comparison_sku_2_name,,Curl Expression 100ml
14 premium_comparison_sku_3_name,,Curl Expression 300ml
15 premium_comparison_attr_1,,Size
16 premium_comparison_sku_1_attr_1,,200ml
17 premium_comparison_sku_2_attr_1,,100ml
18 premium_comparison_sku_3_attr_1,,300ml
19 premium_comparison_attr_2,,Price
20 premium_comparison_sku_1_attr_2,,$24.99
21 premium_comparison_sku_2_attr_2,,$14.99
22 premium_comparison_sku_3_attr_2,,$34.99
23 premium_comparison_attr_3,,Heat Protection
24 premium_comparison_sku_1_attr_3,,Up to 230°C
25 premium_comparison_sku_2_attr_3,,Up to 230°C
26 premium_comparison_sku_3_attr_3,,Up to 230°C
27 premium_comparison_attr_4,,Hair Types
28 premium_comparison_sku_1_attr_4,,All curl types
29 premium_comparison_sku_2_attr_4,,All curl types
30 premium_comparison_sku_3_attr_4,,All curl types
31 premium_comparison_attr_5,,Key Ingredient
32 premium_comparison_sku_1_attr_5,,3% Plant Glycerin
33 premium_comparison_sku_2_attr_5,,3% Plant Glycerin
34 premium_comparison_sku_3_attr_5,,3% Plant Glycerin
35 premium_faq_question_1,,How often should I use this product?
36 premium_faq_answer_1,,Use as needed, typically 2-3 times per week or whenever your curls need extra hydration and definition.
37 premium_faq_question_2,,Can I use this on color-treated hair?
38 premium_faq_answer_2,,Yes, this product is safe for color-treated hair and won't strip or fade your color.
39 premium_faq_question_3,,What's the difference between this and regular conditioner?
40 premium_faq_answer_3,,This is a leave-in treatment that provides continuous hydration and heat protection, while conditioner is rinsed out and provides temporary conditioning.
41 premium_faq_question_4,,Is this product suitable for fine hair?
42 premium_faq_answer_4,,Yes, the lightweight formula won't weigh down fine hair while still providing the hydration and definition your curls need.
43 premium_faq_question_5,,How long does one bottle last?
44 premium_faq_answer_5,,A 200ml bottle typically lasts 2-3 months with regular use, depending on hair length and thickness.