Listicle Creator

Listicle Creator

Craft engaging list-based articles with ease.

Article Setup

List Items

Item 1

Conclusion

Generated Listicle

Copied to clipboard!

${item.description.replace(/\n/g, '
')}

`; }); previewContainer.innerHTML = `

${data.title}

${data.introduction.replace(/\n/g, '
')}

${itemsHTML} ${data.conclusion ? `

Conclusion

${data.conclusion.replace(/\n/g, '
')}

` : ''} `; } function updateItemNumbers() { const items = listItemsContainer.querySelectorAll('.list-item-entry'); items.forEach((item, index) => { const titleElement = item.querySelector('h3'); if (titleElement) { titleElement.textContent = `Item ${index + 1}`; } }); } function addListItemBlock() { const newItem = document.createElement('div'); newItem.className = 'p-4 border border-gray-200 rounded-lg list-item-entry'; newItem.innerHTML = `

New Item

`; listItemsContainer.appendChild(newItem); updateItemNumbers(); } listItemsContainer.addEventListener('click', (e) => { if (e.target.classList.contains('remove-item-button')) { if (listItemsContainer.children.length > 1) { e.target.closest('.list-item-entry').remove(); updateItemNumbers(); } } }); function showMessage(text = 'Copied to clipboard!') { messageBox.textContent = text; messageBox.classList.remove('opacity-0', 'translate-y-10'); messageBox.classList.add('opacity-100', 'translate-y-0'); setTimeout(() => { messageBox.classList.remove('opacity-100', 'translate-y-0'); messageBox.classList.add('opacity-0', 'translate-y-10'); }, 2000); } async function getImageAsBase64(url) { try { const response = await fetch(url); const blob = await response.blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } catch (error) { console.error('Error fetching image for PDF:', error); return null; } } async function generateListiclePdf() { const { jsPDF } = window.jspdf; const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' }); const data = getFormData(); const statusEl = document.getElementById('pdf-status'); pdfDownloadButton.disabled = true; if(statusEl) statusEl.textContent = 'Preparing PDF...'; const pageW = pdf.internal.pageSize.getWidth(); const margin = 15; const contentWidth = pageW - (margin * 2); let y = margin; const lineH = 7; const addHeaderFooter = () => { const pageCount = pdf.internal.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { pdf.setPage(i); pdf.setFontSize(8); pdf.setTextColor('#6b7280'); pdf.text(data.title, margin, pageH - 8); pdf.text(`Page ${i} of ${pageCount}`, pageW - margin, pageH - 8, { align: 'right' }); } const pageH = pdf.internal.pageSize.getHeight(); }; const checkBreak = (needed = 20) => { if (y + needed > pdf.internal.pageSize.getHeight() - margin) { pdf.addPage(); y = margin; } }; const addText = (text, options = {}) => { if (!text) return; const { size=10, style='normal', color='#374151', indent=0, spacing=1 } = options; checkBreak(); pdf.setFontSize(size); pdf.setFont(undefined, style); pdf.setTextColor(color); const splitText = pdf.splitTextToSize(text, contentWidth - indent); pdf.text(splitText, margin + indent, y); y += (splitText.length * lineH * 0.7) + (lineH * spacing); }; // --- Build PDF --- addText(data.title, { size: 20, style: 'bold', color: '#1f2937' }); if(data.author) addText(`By ${data.author}`, { size: 10, style: 'italic', color: '#4b5563' }); y += lineH; addText(data.introduction, { style: 'italic' }); let loadedImages = 0; const imagePromises = data.items.map(item => item.imageUrl ? getImageAsBase64(item.imageUrl) : Promise.resolve(null)); const imageResults = await Promise.all(imagePromises.map(p => p.catch(e => null))); // prevent one failure from stopping all if(statusEl) statusEl.textContent = `Processing ${data.items.length} items...`; for(let i = 0; i < data.items.length; i++) { const item = data.items[i]; const base64Image = imageResults[i]; y += lineH * 1.5; checkBreak(30); addText(`${i + 1}. ${item.title}`, { size: 14, style: 'bold', color: '#6d28d9' }); if (base64Image) { try { const imgProps = pdf.getImageProperties(base64Image); const imgRatio = imgProps.height / imgProps.width; const imgWidth = contentWidth * 0.8; const imgHeight = imgWidth * imgRatio; checkBreak(imgHeight + 5); pdf.addImage(base64Image, imgProps.fileType, margin + (contentWidth * 0.1), y, imgWidth, imgHeight); y += imgHeight + 5; } catch (e) { console.error("Error adding image to PDF:", e); addText(`[Image could not be loaded: ${item.imageUrl}]`, {size: 8, style: 'italic'}); } } addText(item.description); } if (data.conclusion) { y += lineH * 2; addText('Conclusion', {size: 14, style: 'bold', color: '#6d28d9'}); addText(data.conclusion); } addHeaderFooter(); pdf.save(`${data.title.replace(/\s+/g, '_').toLowerCase()}.pdf`); if(statusEl) statusEl.textContent = ''; pdfDownloadButton.disabled = false; } // --- Event Listeners --- tabs.forEach(tab => tab.addEventListener('click', () => goToTab(parseInt(tab.dataset.tab)))); prevButton.addEventListener('click', () => goToTab(currentTab - 1)); nextButton.addEventListener('click', () => goToTab(currentTab + 1)); addListItemButton.addEventListener('click', addListItemBlock); copyButton.addEventListener('click', () => { const data = getFormData(); let text = `${data.title}\n\n`; text += `${data.introduction}\n\n`; data.items.forEach((item, index) => { text += `${index + 1}. ${item.title}\n`; if(item.imageUrl) text += `[Image: ${item.imageUrl}]\n`; text += `${item.description}\n\n`; }); text += `Conclusion\n${data.conclusion}`; navigator.clipboard.writeText(text).then(() => { showMessage(); }).catch(err => console.error('Copy failed: ', err)); }); pdfDownloadButton.addEventListener('click', generateListiclePdf); updateTabUI(); });
Scroll to Top