import { useState, useCallback } from 'react'; /** * PDF Export — block-based strategy. * * Each section is broken into atomic sub-blocks that are never split across * pages. Dark sections with Framer Motion animations require special handling: * we scroll every section into view to trigger `whileInView`, then force all * inline opacity/transform to their final state before html2canvas captures. */ function isDarkSection(section: HTMLElement): boolean { const bg = window.getComputedStyle(section).backgroundColor; if (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') return false; // Parse rgb(r, g, b) — dark if luminance is low const match = bg.match(/(\d+),\s*(\d+),\s*(\d+)/); if (!match) return section.classList.contains('bg-[#0A1128]'); const [, r, g, b] = match.map(Number); return (r * 0.299 + g * 0.587 + b * 0.114) < 50; } function collectSubBlocks(section: HTMLElement): HTMLElement[] { // Dark sections should be captured whole — sub-blocks would lose the dark background if (isDarkSection(section)) return [section]; if (section.offsetHeight <= 900) return [section]; const children = Array.from(section.children) as HTMLElement[]; if (children.length <= 1) return [section]; const blocks: HTMLElement[] = []; for (const child of children) { if (child.offsetHeight === 0 || child.offsetWidth === 0) continue; blocks.push(child); } return blocks.length > 0 ? blocks : [section]; } /** * Scroll through every section to trigger all `whileInView` animations, * then wait for them to settle. This ensures Framer Motion has applied * its final inline styles before we snapshot. */ async function triggerAllAnimations(contentEl: HTMLElement): Promise { const sections = Array.from(contentEl.children) as HTMLElement[]; for (const section of sections) { section.scrollIntoView({ behavior: 'instant' as ScrollBehavior }); // Give framer-motion time to observe intersection and apply styles await new Promise((r) => setTimeout(r, 80)); // Also scroll sub-children into view for nested whileInView const motionChildren = section.querySelectorAll('[style]'); for (let i = 0; i < Math.min(motionChildren.length, 20); i++) { (motionChildren[i] as HTMLElement).scrollIntoView({ behavior: 'instant' as ScrollBehavior }); await new Promise((r) => setTimeout(r, 30)); } } // Scroll back to top and let everything settle window.scrollTo(0, 0); await new Promise((r) => setTimeout(r, 200)); } /** * Force ALL elements to be fully visible — opacity 1, no transforms. * This catches any remaining elements that whileInView didn't reach. */ function forceVisible(contentEl: HTMLElement): (() => void) { document.documentElement.style.setProperty('--motion-duration', '0s'); const saved: { el: HTMLElement; cssText: string }[] = []; contentEl.querySelectorAll('*').forEach((node) => { const el = node as HTMLElement; const s = el.style; // Check both inline and computed const needsFix = (s.opacity !== '' && s.opacity !== '1') || (s.transform !== '' && s.transform !== 'none') || s.visibility === 'hidden'; if (needsFix) { saved.push({ el, cssText: s.cssText }); s.opacity = '1'; s.transform = 'none'; s.visibility = 'visible'; } }); // Second pass: catch computed opacity < 1 (motion might set via class) contentEl.querySelectorAll('*').forEach((node) => { const el = node as HTMLElement; const computed = window.getComputedStyle(el); if (parseFloat(computed.opacity) < 0.99) { if (!saved.find((s) => s.el === el)) { saved.push({ el, cssText: el.style.cssText }); } el.style.opacity = '1'; el.style.transform = 'none'; } }); return () => { saved.forEach(({ el, cssText }) => { el.style.cssText = cssText; }); document.documentElement.style.removeProperty('--motion-duration'); }; } /** * CSS overrides for export: unwrap overflow, prevent clipping. */ function addExportCSS(): (() => void) { const style = document.createElement('style'); style.id = 'pdf-export-overrides'; style.textContent = ` [data-report-content] .overflow-x-auto, [data-plan-content] .overflow-x-auto { overflow: visible !important; flex-wrap: wrap !important; } [data-report-content] .scrollbar-thin, [data-plan-content] .scrollbar-thin { overflow: visible !important; } [data-report-content] .shrink-0, [data-plan-content] .shrink-0 { flex-shrink: 1 !important; min-width: 0 !important; } `; document.head.appendChild(style); return () => style.remove(); } export function useExportPDF() { const [isExporting, setIsExporting] = useState(false); const exportPDF = useCallback(async (filename = 'INFINITH_Marketing_Intelligence_Report') => { setIsExporting(true); try { const [{ default: html2canvas }, { jsPDF }] = await Promise.all([ import('html2canvas-pro'), import('jspdf'), ]); const contentEl = (document.querySelector('[data-report-content]') as HTMLElement) || (document.querySelector('[data-plan-content]') as HTMLElement); if (!contentEl) throw new Error('Report content element not found'); // Step 1: Scroll through all sections to trigger whileInView animations await triggerAllAnimations(contentEl); // Step 2: Force everything visible (catch stragglers) const restoreVisible = forceVisible(contentEl); // Step 3: CSS overrides const restoreCSS = addExportCSS(); // Step 4: Hide UI elements const hideSelectors = [ '[data-report-nav]', '[data-plan-nav]', 'nav', '[data-cta-card]', '[data-no-print]', ]; const hiddenEls: { el: HTMLElement; display: string }[] = []; hideSelectors.forEach((sel) => { document.querySelectorAll(sel).forEach((el) => { const htmlEl = el as HTMLElement; hiddenEls.push({ el: htmlEl, display: htmlEl.style.display }); htmlEl.style.display = 'none'; }); }); await new Promise((r) => setTimeout(r, 150)); // Step 5: PDF generation const pdf = new jsPDF('p', 'mm', 'a4'); const pageWidth = 210; const pageHeight = 297; const margin = 8; const usableWidth = pageWidth - margin * 2; const footerSpace = 10; const maxContentY = pageHeight - margin - footerSpace; let currentY = margin; const sections = Array.from(contentEl.children) as HTMLElement[]; for (const section of sections) { if (section.offsetHeight === 0 || section.offsetWidth === 0) continue; if (window.getComputedStyle(section).display === 'none') continue; const blocks = collectSubBlocks(section); for (const block of blocks) { if (block.offsetHeight === 0) continue; const canvas = await html2canvas(block, { scale: 2, useCORS: true, logging: false, backgroundColor: null, windowWidth: 1280, removeContainer: true, }); const blockHeightMM = (canvas.height * usableWidth) / canvas.width; // New page if block doesn't fit if (currentY + blockHeightMM > maxContentY && currentY > margin + 5) { pdf.addPage(); currentY = margin; } // Tall block: slice across pages if (blockHeightMM > maxContentY - margin) { const pxPerMM = canvas.width / usableWidth; let srcY = 0; let remainPx = canvas.height; let isFirst = true; while (remainPx > 0) { if (!isFirst) { pdf.addPage(); currentY = margin; } isFirst = false; const availPx = (maxContentY - currentY) * pxPerMM; const sliceH = Math.min(remainPx, availPx); const sliceCanvas = document.createElement('canvas'); sliceCanvas.width = canvas.width; sliceCanvas.height = Math.ceil(sliceH); const ctx = sliceCanvas.getContext('2d'); if (ctx) { ctx.drawImage(canvas, 0, srcY, canvas.width, Math.ceil(sliceH), 0, 0, canvas.width, Math.ceil(sliceH)); } const sliceMM = sliceH / pxPerMM; pdf.addImage(sliceCanvas.toDataURL('image/jpeg', 0.9), 'JPEG', margin, currentY, usableWidth, sliceMM); currentY += sliceMM; srcY += sliceH; remainPx -= sliceH; } } else { pdf.addImage(canvas.toDataURL('image/jpeg', 0.9), 'JPEG', margin, currentY, usableWidth, blockHeightMM); currentY += blockHeightMM; } } currentY += 2; // Gap between sections } // Step 6: Restore hiddenEls.forEach(({ el, display }) => { el.style.display = display; }); restoreCSS(); restoreVisible(); // Step 7: Footers const totalPages = pdf.getNumberOfPages(); const footerLabel = filename.includes('Plan') ? 'INFINITH Marketing Plan' : 'INFINITH Marketing Intelligence Report'; for (let i = 1; i <= totalPages; i++) { pdf.setPage(i); pdf.setFontSize(7); pdf.setTextColor(180); pdf.text(`${footerLabel} | Page ${i} / ${totalPages}`, pageWidth / 2, pageHeight - 5, { align: 'center' }); } pdf.save(`${filename}.pdf`); } catch (err) { console.error('PDF export failed:', err); } finally { setIsExporting(false); } }, []); return { exportPDF, isExporting }; }