import React, { useEffect, useRef, useState } from 'react'; import { ArrowLeft, Download, FileText, CheckCircle2, ExternalLink } from 'lucide-react'; import { PaperSize, DocxStyleConfig, DocxBorder } from '../types'; import { TYPOGRAPHY_STYLES } from '../constants'; // @ts-ignore import * as docx from 'docx'; interface PreviewProps { htmlContent: string; onBack: () => void; paperSize: PaperSize; selectedStyleId?: string | null; } export const Preview: React.FC = ({ htmlContent, onBack, paperSize, selectedStyleId }) => { const iframeRef = useRef(null); const [isExporting, setIsExporting] = useState(false); const [successMsg, setSuccessMsg] = useState(false); // Get current style const style = TYPOGRAPHY_STYLES.find(s => s.id === selectedStyleId) || TYPOGRAPHY_STYLES[0]; // Extract unique fonts for display const usedFonts = Array.from(new Set([ style.wordConfig.heading1.font, style.wordConfig.heading2.font, style.wordConfig.body.font ])).filter(Boolean); // Helper to convert Points to Half-Points (docx standard) const pt = (points: number) => points * 2; // Helper to convert Inches/MM to Twips const inchesToTwips = (inches: number) => Math.round(inches * 1440); const mmToTwips = (mm: number) => Math.round(mm * (1440 / 25.4)); // Helper to map Border config to Docx Border const mapBorder = (b?: DocxBorder) => { if (!b) return undefined; let style = docx.BorderStyle.SINGLE; if (b.style === 'double') style = docx.BorderStyle.DOUBLE; if (b.style === 'dotted') style = docx.BorderStyle.DOTTED; if (b.style === 'dashed') style = docx.BorderStyle.DASHED; return { color: b.color, space: b.space, style: style, size: b.size }; }; const generateDocx = async (styleId: string) => { setIsExporting(true); try { const style = TYPOGRAPHY_STYLES.find(s => s.id === styleId) || TYPOGRAPHY_STYLES[0]; const cfg = style.wordConfig; // PARSE HTML const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, 'text/html'); const nodes = Array.from(doc.body.childNodes); const docxChildren = []; for (const node of nodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; const el = node as HTMLElement; const tagName = el.tagName.toLowerCase(); // --- Run Parser (Inline Styles) --- const parseRuns = (element: HTMLElement, baseConfig: DocxStyleConfig) => { const runs = []; for (const child of Array.from(element.childNodes)) { if (child.nodeType === Node.TEXT_NODE) { runs.push(new docx.TextRun({ text: child.textContent || '', font: baseConfig.font, size: pt(baseConfig.size), color: baseConfig.color, underline: baseConfig.underline ? { type: docx.UnderlineType.SINGLE, color: baseConfig.color } : undefined, })); } else if (child.nodeType === Node.ELEMENT_NODE) { const childEl = child as HTMLElement; const isBold = childEl.tagName === 'STRONG' || childEl.tagName === 'B'; const isItalic = childEl.tagName === 'EM' || childEl.tagName === 'I'; runs.push(new docx.TextRun({ text: childEl.textContent || '', bold: isBold, italics: isItalic, font: baseConfig.font, size: pt(baseConfig.size), color: baseConfig.color, underline: baseConfig.underline ? { type: docx.UnderlineType.SINGLE, color: baseConfig.color } : undefined, })); } } return runs; }; // --- Block Parser --- // 1. HEADINGS if (tagName.match(/^h[1-6]$/)) { const level = parseInt(tagName.replace('h', '')); // Select config based on level (simplified to H1 or H2, fallback H2 for others) const hCfg = level === 1 ? cfg.heading1 : cfg.heading2; const headingLevel = level === 1 ? docx.HeadingLevel.HEADING_1 : docx.HeadingLevel.HEADING_2; // Border Mapping const borderConfig: any = {}; if (hCfg.border) { if (hCfg.border.top) borderConfig.top = mapBorder(hCfg.border.top); if (hCfg.border.bottom) borderConfig.bottom = mapBorder(hCfg.border.bottom); if (hCfg.border.left) borderConfig.left = mapBorder(hCfg.border.left); if (hCfg.border.right) borderConfig.right = mapBorder(hCfg.border.right); } // Alignment Mapping let align = docx.AlignmentType.LEFT; if (hCfg.align === 'center') align = docx.AlignmentType.CENTER; if (hCfg.align === 'right') align = docx.AlignmentType.RIGHT; if (hCfg.align === 'both') align = docx.AlignmentType.BOTH; docxChildren.push(new docx.Paragraph({ children: [ new docx.TextRun({ text: el.textContent || '', font: hCfg.font, bold: hCfg.bold, italics: hCfg.italic, underline: hCfg.underline ? { type: docx.UnderlineType.SINGLE, color: hCfg.color } : undefined, size: pt(hCfg.size), color: hCfg.color, allCaps: hCfg.allCaps, smallCaps: hCfg.smallCaps, characterSpacing: hCfg.tracking }) ], heading: headingLevel, alignment: align, spacing: { before: hCfg.spacing?.before, after: hCfg.spacing?.after, line: hCfg.spacing?.line }, border: borderConfig, shading: hCfg.shading ? { fill: hCfg.shading.fill, color: hCfg.shading.color, type: docx.ShadingType.CLEAR // usually clear to show fill } : undefined, keepNext: true, keepLines: true })); } // 2. PARAGRAPHS else if (tagName === 'p') { let align = docx.AlignmentType.LEFT; if (cfg.body.align === 'center') align = docx.AlignmentType.CENTER; if (cfg.body.align === 'right') align = docx.AlignmentType.RIGHT; if (cfg.body.align === 'both') align = docx.AlignmentType.BOTH; docxChildren.push(new docx.Paragraph({ children: parseRuns(el, cfg.body), spacing: { before: cfg.body.spacing?.before, after: cfg.body.spacing?.after, line: cfg.body.spacing?.line, lineRule: docx.LineRuleType.AUTO }, alignment: align })); } // 3. BLOCKQUOTES else if (tagName === 'blockquote') { docxChildren.push(new docx.Paragraph({ children: parseRuns(el, { ...cfg.body, size: cfg.body.size + 1, color: cfg.accentColor, italic: true } as DocxStyleConfig), indent: { left: 720 }, // 0.5 inch border: { left: { color: cfg.accentColor, space: 10, style: docx.BorderStyle.SINGLE, size: 24 } }, shading: { fill: "F8F8F8", type: docx.ShadingType.CLEAR, color: "auto" }, // Default light grey background for quotes spacing: { before: 200, after: 200, line: 300 } })); } // 4. LISTS else if (tagName === 'ul' || tagName === 'ol') { const listItems = Array.from(el.children); for (const li of listItems) { docxChildren.push(new docx.Paragraph({ children: parseRuns(li as HTMLElement, cfg.body), bullet: { level: 0 }, spacing: { before: 80, after: 80 } })); } } } // Create Document const docxFile = new docx.Document({ sections: [{ properties: { page: { size: { width: paperSize === 'A4' ? mmToTwips(210) : inchesToTwips(8.5), height: paperSize === 'A4' ? mmToTwips(297) : inchesToTwips(11), }, margin: { top: inchesToTwips(1), right: inchesToTwips(1.2), bottom: inchesToTwips(1), left: inchesToTwips(1.2), } } }, children: docxChildren }] }); const blob = await docx.Packer.toBlob(docxFile); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `typogenie-${style.name.replace(/\s+/g, '-').toLowerCase()}.docx`; a.click(); window.URL.revokeObjectURL(url); setSuccessMsg(true); setTimeout(() => setSuccessMsg(false), 3000); } catch (e) { console.error("Docx Gen Error", e); alert("Failed to generate DOCX"); } finally { setIsExporting(false); } }; useEffect(() => { if (!iframeRef.current) return; // We already have 'style' from the component scope, but useEffect needs to be robust const currentStyle = TYPOGRAPHY_STYLES.find(s => s.id === selectedStyleId) || TYPOGRAPHY_STYLES[0]; const doc = iframeRef.current.contentDocument; if (doc) { const html = `
${htmlContent}
`; doc.open(); doc.write(html); doc.close(); } }, [htmlContent, paperSize, selectedStyleId]); return (
{/* Toolbar */}
{/* Font List */}
Fonts:
{usedFonts.map(font => ( {font} ))}
Format: {paperSize}