292 lines
23 KiB
HTML
Executable file
292 lines
23 KiB
HTML
Executable file
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Amazon 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>
|